Система уведомлений
Назначение
Система уведомлений разделяет три задачи:
- in-app notification center хранит важные события для пользователя;
/ws/notificationsдоставляет новые in-app уведомления в открытый интерфейс в реальном времени;notifications-workerотправляет email через outbox и ретраи.
Frontend не отправляет email и не решает, кому можно получить событие. Он подключается к WebSocket, показывает badge/toast и вызывает REST endpoint-ы для чтения и архивации.
Каналы
Поддерживаемые каналы v1:
in_app— запись вnotifications, отображается в UI;email— delivery-запись вnotification_deliveries, отправляется отдельным worker.
Пользовательские настройки берутся из users.notification_preferences. Если in_app=false, in-app запись не создается. Если email=false, email delivery не создается.
API
Основные endpoint-ы:
GET /api/v1/notifications?unread_only=true— список уведомлений текущего пользователя;GET /api/v1/notifications/unread-count— счетчик непрочитанных;POST /api/v1/notifications/:id/read— отметить одно уведомление прочитанным;POST /api/v1/notifications/read-all— отметить все уведомления прочитанными;DELETE /api/v1/notifications/:id— soft archive.
WebSocket:
/ws/notificationsотправляет события:notification_created— новое уведомление (notification);notification_read— уведомления прочитаны (notification_id— первое,notification_ids— все), в том числе при прочтении чата (POST /chats/:id/readгасит егоchat.message/chat.mention);notifications_read_all— прочитаны все;notification_archived— уведомлениеnotification_idудалено;
- сервер шлёт ping-кадр каждые 30 секунд, клиент —
{"type":"heartbeat"}каждые 30 секунд (иначе через 70 секунд без кадров соединение закрывается); после переподключения клиент перечитывает список и счётчик; - счётчик на колокольчике —
GET /notifications/unread-count, а не число непрочитанных среди первых 50; /ws/chatостается каналом realtime-сообщений чата.
Браузерный WebSocket() не умеет слать заголовок Authorization, поэтому подключение идёт по одноразовому тикету: access token в URL оседал бы в логах прокси и в истории.
POST /api/v1/ws/ticketс обычной аутентификацией →{"ticket": "…", "expires_at": "…"}. Тикет живёт минуту и гасится при первом использовании.ws://<host>/ws/notifications?ticket=<ticket>.- На каждое переподключение запрашивается новый тикет.
Небраузерные клиенты используют Authorization: Bearer <token>. Старый способ ?token=<access_token> продолжает работать, но считается устаревшим.
Каждое уведомление содержит готовое поле message для отображения. Это поле дублирует payload.message и заполняется backend-ом, чтобы frontend не собирал текст уведомления самостоятельно.
Для чатовых уведомлений payload дополнительно содержит:
{
"chat_id": 10,
"message_id": 99,
"from_user_id": 1,
"from_user_name": "Мария Иванова",
"message_text": "Привет, проверь задачу",
"preview": "Привет, проверь задачу",
"message": "Мария Иванова: Привет, проверь задачу"
}Delivery worker
Email отправляется не из HTTP request path. Backend создает delivery-запись со статусом pending, а notifications-worker забирает партии через PostgreSQL FOR UPDATE SKIP LOCKED.
Статусы delivery:
pending— ожидает отправки или retry;processing— взято worker-ом;sent— отправлено;failed— исчерпаны попытки.
В Docker Compose backend запускается с APP_RUN_BACKGROUND_WORKERS=false, а notifications-worker запускает фоновые задачи и email delivery. Для локального запуска без отдельного worker значение по умолчанию остается true.
События v1
Подключенные события:
task.assigned— назначение основному исполнителю и новым co-assignees;chat.message— обычное новое сообщение всем участникам чата, кроме автора; толькоin_app, без email delivery;chat.mention— упоминание пользователя черезmention_user_ids;chat.ping— ping пользователя в чате;project.member_added— пользователя добавили в проектную команду; толькоin_app, без email delivery;team_request.submitted— менеджерам отделов по позициям заявки;team_request.approved/team_request.rejected— автору заявки;team_request.item_assigned— автору заявки и назначенному сотруднику;project_spec.submitted— менеджеру проекта;project_spec.approved/project_spec.rejected— менеджеру проекта;time_entries_missing_personal;time_entries_missing_manager.
Обычные новые сообщения чата доставляются двумя каналами: /ws/chat обновляет открытую панель чата, а chat.message создает запись в notification center и realtime-событие /ws/notifications для badge/колокольчика. Email для chat.message не ставится в outbox. Уведомление содержит имя отправителя и текст сообщения; упоминания получают chat.mention с теми же полями payload.
Если админ отправляет сообщение в чат, где он раньше не был участником, backend добавляет его в участники перед созданием сообщения. Это нужно, чтобы последующие ответы пользователей создавали chat.message для админа так же, как для обычного участника чата.
Оповещение в браузере
Новое уведомление, пришедшее по /ws/notifications (action: created, ещё не прочитанное), фронт показывает сразу (useNotificationAlerts):
- Звук — короткий двухнотный сигнал, только для
chat.messageиchat.mention; пачка сообщений подряд даёт один сигнал (не чаще раза в 1,5 с). Звук синтезируется Web Audio, файла нет; браузер разрешает его после первого нажатия на странице. - Всплывающее окошко — тост «Автор: текст» с кнопкой «Открыть» (переход по маршруту уведомления, уведомление отмечается прочитанным).
- Рабочий стол — если вкладка свёрнута или окно не в фокусе и браузер разрешил уведомления, вместо окошка показывается системное уведомление; сообщения одного чата заменяют друг друга (
tag: chat-<id>). Разрешение предлагается в колокольчике и в профиле. - Счётчик во вкладке — «(3) Чаты — RMS» по числу непрочитанных колокольчика.
Сообщение чата, который открыт на экране (раздел «Чаты», чат справа, чат задачи) при видимой вкладке в фокусе, не оповещается. Настройки «Звук», «Всплывающие окошки», «На рабочем столе» — профиль → «Уведомления», хранятся в браузере (localStorage, ключ rms-notification-alerts), по умолчанию всё включено.
Расширение
Следующие бизнес-события должны подключаться через тот же notifications.Service.Notify:
- equipment requests: submitted/finance approved/rejected/procurement started;
- trips: approval requested/approved/cancelled;
- due soon/overdue/status changed для задач.
По этому же WebSocket приходят изменения сущностей (entity_changed) — см. Синхронизация между пользователями.