Corp/docs/guides/NOTIFICATIONS.md
lizerk 14f1e52425
Some checks failed
Deploy production / deploy (push) Failing after 4s
fix(messages): make HTTP authoritative for chat reads
2026-09-10 11:41:59 +03:00

189 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Уведомления
Текущая система уведомлений — 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`.