# Уведомления Текущая система уведомлений — verb-based Django app с хранением в PostgreSQL и доставкой через WebSocket, email и Web Push. ## Источники истины - models: `backend/notifications/models.py` - создание: `backend/notifications/signals.py` - channel routing: `backend/notifications/channels.py` - REST API: `backend/notifications/api/` - senders/tasks: `backend/notifications/senders/` и `backend/notifications/tasks/` - realtime state events: `backend/notifications/realtime.py` - frontend context: `frontend/src/contexts/NotificationsContext.tsx` - Web Push hook: `frontend/src/hooks/useWebPush.ts` - tests: `backend/tests/api/v1/notifications/` и `backend/tests/realtime/` `NotificationService` в `backend/notifications/services.py` — deprecated stub и выбрасывает `NotImplementedError`. Новый код обязан использовать `notify.send(...)`. ## Создание уведомления ```python from notifications.signals import notify notify.send( sender=request.user, recipient=recipient, verb="document_assigned", target=document, description="Вам назначен документ", action_url=f"/documents/{document.pk}", data={ "title": "Новый документ", "document_id": document.pk, }, ) ``` `recipient` может быть одним `Employee` или списком/tuple сотрудников. Обязательны `recipient` и `verb`. `actor`, `action_object` и `target` хранятся через GenericForeignKey. Не создавайте уведомления пользователю о его собственном действии, если продуктовый сценарий явно этого не требует. `action_url` должен быть внутренним разрешённым маршрутом frontend, а `data` — JSON-serializable. Для сообщений verb `chat_mention` создаётся только по проверенному структурированному полю `Message.mentions`, а не по произвольному тексту после `@`. Получатель упоминания исключается из обычного `chat_new_message` или `chat_reply` для этого же сообщения, поэтому одно действие не создаёт два уведомления. Старое `notifications_enabled=false` мигрирует в режим `mentions`: обращения продолжают приходить. Явный режим `muted` и временная пауза подавляют все события чата. ## Поток доставки 1. `notify.send` создаёт `Notification`. 2. `post_save` проверяет `UserChannelPreferences`, disabled verb и DND. 3. Через `transaction.on_commit` запускаются Celery tasks. 4. WebSocket sender отправляет событие в `user_{id}`. 5. Email sender отправляет instant email либо digest. 6. Push sender выбирает активные устройства и маршруты приложений. Если Celery broker недоступен, создание записи уведомления не откатывается: ошибка постановки задачи логируется. Повторная доставка требует отдельной операционной проверки. ## Модели `Notification` содержит recipient, actor, verb, action object, target, description, action URL, JSON data, unread/deleted/public/emailed flags и timestamps. Удаление через API мягкое (`deleted=True`). `UserChannelPreferences` управляет: - `web_enabled`; - `email_enabled` и `email_frequency`: `instant`, `daily`, `weekly`, `never`; - `push_enabled`; - DND interval; - `disabled_verbs`. При DND отправляется только silent WebSocket, если web channel включён. Email и push в этот момент не ставятся в очередь. `NotificationInstallation` связывает Web Push subscription с приложением `main` или `messenger` и device group. `NotificationDeliveryRouting` выбирает приложение доставки по категориям: messages, tasks, requests, procurement, documents, guests, calendar, feed, other. `ChatUserSettings` дополнительно управляет режимом конкретного чата (`all`, `mentions`, `muted`), `muted_until`, показом текста в системном уведомлении и звуком. Endpoint `POST /api/v1/communications/chats/{id}/notifications/` принимает эти поля; пустой POST сохранён как совместимый быстрый переключатель `all` ↔ `mentions`. ## REST API Prefix: `/api/v1/notifications/`. | Method и path | Назначение | | --- | --- | | `GET /` | Список; filters `page`, `page_size`, `verb`, `unread_only`, `search` | | `GET /count/` | Общее количество непрочитанных | | `GET /summary/` | Агрегаты unread для навигации и доменных экранов | | `GET /verb-types/` | Доступные verbs и счётчики | | `POST /{id}/read/` | Отметить одно прочитанным | | `POST /{id}/unread/` | Вернуть в непрочитанные | | `DELETE /{id}/` | Soft-delete | | `POST /read-all/` | Прочитать все или один verb | | `POST /category/read/` | Прочитать список verbs категории | | `DELETE /delete-all-read/` | Soft-delete всех прочитанных | | `GET, PUT /preferences/` | Настройки каналов | | `GET /push/vapid-key/` | Публичный VAPID key | | `POST /push/subscribe/` | Создать/обновить push subscription | | `POST, DELETE /push/unsubscribe/` | Удалить одну или все subscriptions | | `POST /push/installations/heartbeat/` | Обновить installation lease | | `GET, PUT /push/delivery-routing/` | Маршрутизация main/messenger | | `DELETE /push/installations/disconnect/` | Отключить приложение на устройстве | Endpoint `/push/subscriptions/` отсутствует. Не используйте старые примеры с `/api/v2/notifications/` или `/ws/notifications/`. ## Realtime events Через единый `/ws/` frontend получает: - `notification` — новая запись; - `unread_count` — актуальный общий счётчик; - `notification_read` — прочтение в другой вкладке/сессии; - `notifications_read_all` — массовое прочтение. - `notifications_deleted` — удаление записей из всех открытых сессий. `NotificationsContext` объединяет REST initial load и realtime delta, подавляет дубли и выполняет fallback refresh. Не поднимайте отдельный notifications WebSocket. ## Web Push Backend требует `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` и `VAPID_ADMIN_EMAIL`. Subscription сохраняется в `django-push-notifications` `WebPushDevice` и связывается с `NotificationInstallation`. Frontend flow: 1. проверяет browser support и permission; 2. получает VAPID public key; 3. регистрирует service worker; 4. отправляет installation heartbeat; 5. только по user gesture запрашивает permission и создаёт subscription; 6. передаёт endpoint, `p256dh`, `auth`, installation/device group и app kind backend-у. Push работает только в secure context, кроме localhost. Permission `denied` нельзя повторно запросить из JavaScript — пользователь должен изменить настройку сайта в браузере. При permanent delivery error (`404`, `410`, expired/unregistered subscription) device деактивируется и отвязывается от installation. Transient error не удаляет устройство. Для messenger-only route неуспешная доставка может fallback-нуться на main installation того же device group. Устаревшая messenger installation также возвращает доставку в main. Уведомления сообщений используют стабильный `tag=chat-{chat_id}` и несут `chat_id`, `message_id`, `notification_id`. Backend отправляет невидимые control push-команды, а `frontend/public/sw.js` применяет их через `getNotifications()`: прочтение закрывает уведомление на остальных устройствах, удаление сообщения убирает относящийся к нему push, редактирование заменяет только уже показанный push. Если исходная Celery-задача задержалась, sender повторно проверяет `unread/deleted` и не показывает устаревшее уведомление. Активный чат хранится как `ActiveChatSession` с TTL 75 секунд и продлевается WebSocket heartbeat. Presence используется только для подавления нативного Web Push уже открытого чата; она не отмечает notification прочитанным. Событие состояния notification продолжает приходить по WebSocket, а read state изменяется гарантированной HTTP-операцией вместе с курсором сообщения и затем синхронизируется между вкладками. После добавления или изменения Celery tasks worker требуется перезапустить — Django autoreload задачи Celery не обновляет. ## Email `instant` ставит отдельную Celery task при создании уведомления. `daily` и `weekly` обрабатываются Celery Beat в 09:00 по timezone проекта. `never` отключает email. Проверьте `EMAIL_BACKEND`, SMTP settings, `DEFAULT_FROM_EMAIL` и `SITE_URL`. В DEBUG по умолчанию используется console email backend. ## Настройки ```dotenv REDIS_HOST=127.0.0.1 REDIS_PORT=6379 CELERY_BROKER_URL=redis://127.0.0.1:6379/0 CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/1 VAPID_PUBLIC_KEY=change-me VAPID_PRIVATE_KEY=change-me VAPID_ADMIN_EMAIL=admin@example.com SITE_URL=http://localhost:8000 ``` Запуск worker и Beat: ```bash cd backend celery -A eusrr_backend worker -l info celery -A eusrr_backend beat -l info ``` ## Добавление нового verb 1. Выберите стабильное имя в `snake_case` с доменным prefix. 2. Добавьте translation/category mapping на frontend, если verb виден пользователю. 3. Передавайте title в `data.title`, если техническое имя verb не подходит как заголовок. 4. Передавайте ID доменного объекта в `data` для unread summary/navigation. 5. Не отправляйте секреты и большой payload. 6. Добавьте тест создания и routing по каналам. 7. Добавьте тест action URL и self-notification rule. ## Проверка ```bash cd backend pytest tests/api/v1/notifications tests/realtime -q ``` Для ручной проверки создайте уведомление через `notify.send`, затем проверьте REST list, Celery log, WebSocket event и push device. Не вызывайте sender напрямую: это обходит channel preferences, DND и transaction boundary. ## Диагностика - Запись есть, realtime нет: проверьте Celery task `notifications.send_websocket_notification`, Redis channel layer и подписку на `user_{id}`. - Push не создаётся: проверьте HTTPS/localhost, permission, service worker и VAPID public key. - Push создан, но не приходит: проверьте `push_enabled`, active `WebPushDevice`, installation route и Celery worker. - Уведомление дублируется: найдите повторный `notify.send`; UI deduplication не должна маскировать backend double-create. - Unread count устарел: операции read должны идти через API/model methods, которые инвалидируют cache и отправляют realtime state event. - Email не приходит: проверьте frequency, DND, SMTP и поле `emailed`.