Push-уведомления
Устройства и согласие пользователя, автоматические события, кастомные рассылки, аудитории, права кураторов и гарантии доставки.
Как устроена доставка
Веб-клиент получает FCM token только после явного действия пользователя и сохраняет его через /notifications/me/devices. Сервер шифрует token, создаёт push-outbox по событию или кампании, разворачивает запись в доставки по активным устройствам и отправляет data-only сообщение через Firebase Cloud Messaging. Service Worker показывает системное уведомление; в открытом приложении сообщение отображается как toast.
notification_preferences.push_enabled— общий выключатель аккаунта. Он не заменяет разрешение браузера.push_devices.enabled— выключатель конкретного устройства. Одна установка определяется стабильнымinstallation_id; повторная регистрация обновляет ротированный FCM token.- Обычный выход удаляет binding текущей установки и локальный Firebase token best-effort. Другие устройства аккаунта не затрагиваются.
- Plaintext token никогда не возвращается API и хранится на сервере только в зашифрованном виде; для поиска используется SHA-256 hash.
Настройки пользователя
На странице «Настройки» ученик, куратор и администратор могут включить или выключить push для аккаунта, зарегистрировать текущий браузер, отключить отдельное устройство или удалить его. При включении клиент одновременно запрашивает browser permission, получает FCM token, выполняет идемпотентный upsert устройства и включает preference. GET /notifications/me отдаёт пагинированную историю успешно доставленных push без дублей между устройствами.
Доступ администратора и куратора
Администратор всегда видит страницу /notifications. Куратору администратор выдаёт отдельное право can_manage_notifications в разделе «Команда». Все management endpoints защищены тем же server-side permission; скрытие пункта меню не является защитой.
Каталог автоматических событий
Каталог содержит 63 стабильных кода, и у всех текущих событий producer подключён. Поле implemented остаётся частью API-контракта: если в будущем событие появится раньше источника, оно будет показано как «Запланировано» и останется недоступным для включения.
| Код | Когда возникает | Статус |
|---|---|---|
account.welcome | Добро пожаловать / аккаунт создан | Работает · device_registration |
account.activated | Аккаунт снова активен | Работает · audit_sink |
account.deactivated | Аккаунт отключён | Работает · audit_sink |
account.role_changed | Пользователю назначена другая роль | Работает · audit_sink |
account.password_changed | Пароль успешно изменён | Работает · audit_sink |
group.member_added | Студент добавлен в группу | Работает · audit_sink |
group.member_removed | Студент удалён из группы | Работает · audit_sink |
group.curator_assigned | Куратору назначена группа | Работает · audit_sink |
group.curator_unassigned | Куратор снят с группы | Работает · audit_sink |
student.curator_assigned | Куратору индивидуально назначен ученик | Работает · audit_sink |
student.curator_unassigned | С куратора снято индивидуальное назначение ученика | Работает · audit_sink |
course.enrolled | Зачисление на курс | Работает · audit_sink |
course.unenrolled | Отчисление с курса | Работает · audit_sink |
course.published | Доступный курс опубликован | Работает · audit_sink |
course.unpublished | Курс снят с публикации | Работает · audit_sink |
course.content_added | В курс добавлен материал | Работает · audit_sink |
course.content_updated | Материал курса обновлён | Работает · audit_sink |
course.available | Наступило окно доступа к курсу | Работает · schedule_scan |
module.available | Наступило окно доступа к модулю | Работает · schedule_scan |
lesson.available | Наступило окно доступа к уроку | Работает · schedule_scan |
lesson.deadline_reminder | Ежедневное напоминание за N дней | Работает · deadline_scan |
lesson.deadline_today | Последний день доступа к уроку | Работает · deadline_scan |
lesson.access_closed | Окно доступа к уроку закрыто | Работает · deadline_scan |
assignment.assigned | Задание стало доступно | Работает · audit_sink |
assignment.updated | Задание изменено | Работает · audit_sink |
assignment.submitted | Работа отправлена на проверку | Работает · audit_sink |
assignment.accepted | Работа принята куратором | Работает · audit_sink |
assignment.rejected | Работа возвращена на доработку | Работает · audit_sink |
assignment.score_updated | Оценка задания изменена | Работает · audit_sink |
assignment.reopened | Открыта повторная отправка | Работает · audit_sink |
assignment.deadline_reminder | Скоро закроется урок с заданием | Работает · deadline_scan |
quiz.assigned | Тест стал доступен | Работает · audit_sink |
quiz.attempt_started | Студент начал попытку теста | Работает · audit_sink |
quiz.submitted | Тест завершён | Работает · audit_sink |
quiz.passed | Тест пройден | Работает · audit_sink |
quiz.failed | Тест не пройден | Работает · audit_sink |
quiz.review_required | Нужна ручная проверка | Работает · audit_sink |
quiz.score_updated | Результат изменён после проверки | Работает · audit_sink |
quiz.auto_finalized | Попытка завершена по таймеру | Работает · audit_sink |
exam.published | Пробный ЕНТ опубликован | Работает · audit_sink |
exam.attempt_started | Студент начал пробный ЕНТ | Работает · audit_sink |
exam.submitted | Пробный ЕНТ завершён | Работает · audit_sink |
exam.passed | Пробный ЕНТ сдан | Работает · audit_sink |
exam.failed | Пробный ЕНТ не сдан | Работает · audit_sink |
exam.auto_finalized | Пробный ЕНТ завершён по таймеру | Работает · audit_sink |
progress.block_completed | Завершён блок урока | Работает · audit_sink |
progress.lesson_completed | Завершён урок | Работает · audit_sink |
progress.module_completed | Завершён модуль | Работает · audit_sink |
progress.course_completed | Завершён курс | Работает · audit_sink |
progress.milestone | Достигнут процент прогресса | Работает · audit_sink |
progress.stalled | Студент давно не учился | Работает · schedule_scan |
learning_path.course_unlocked | Открыт следующий курс траектории | Работает · audit_sink |
learning_path.completed | Траектория завершена | Работает · audit_sink |
gamification.points_awarded | Автоматически начислены баллы | Работает · audit_sink |
gamification.manual_adjustment | Баллы изменены вручную | Работает · audit_sink |
gamification.rank_changed | Изменена позиция в рейтинге | Работает · audit_sink |
billing.expiry_reminder | Подписка школы скоро закончится | Работает · deadline_scan |
billing.expired | Подписка закончилась | Работает · audit_sink |
billing.activated | Подписка активирована | Работает · audit_sink |
billing.extended | Подписка продлена | Работает · audit_sink |
billing.suspended | Школа приостановлена | Работает · audit_sink |
billing.unsuspended | Доступ к школе восстановлен | Работает · audit_sink |
media.processing_failed | Ошибка обработки медиа | Работает · audit_sink |
media.delete_failed | Ошибка фонового удаления медиа | Работает · audit_sink |
campaign.custom | Кастомная рассылка | Работает · admin_campaign |
Напоминание о дедлайне урока
lesson.deadline_reminder использует фактическое ends_at окна урока. По умолчанию scheduler начинает за 3 дня, повторяет каждый день и отправляет в 09:00 локального времени школы. Настройки: days_before — целое 1…90, repeat_daily — boolean, send_at_local — HH:MM. Dedupe key не допускает повтор одной и той же рассылки за день.
Кастомные кампании
Кампания отправляется сразу или по scheduled_at. Перед созданием UI вызывает server-side preview. После подтверждения сервер повторно вычисляет аудиторию, фиксирует immutable snapshot получателей и возвращает фактический audience_count; изменения групп, прогресса или фильтров уже не меняют эту кампанию.
| Поле правила | Операторы | Сегмент |
|---|---|---|
student_id | in, not_in | Конкретные ученики |
group_id | in, not_in | Текущий состав групп |
course_enrollment, course_completed, course_in_progress, course_not_completed | in, not_in | Зачисление и состояние прохождения курса |
module_completed, lesson_completed | in, not_in | Полностью завершили модуль / урок |
quiz_attempted, quiz_passed, quiz_failed | in, not_in | Результаты конкретных тестов |
quiz_score_pct | gte, lte | Результат выбранных тестов 0…100% (values + value) |
assignment_submitted, assignment_accepted, assignment_rejected | in, not_in | Статус конкретных заданий |
assignment_score_pct | gte, lte | Оценка выбранных заданий 0…100% (values + value) |
exam_attempted, exam_passed, exam_failed | in, not_in | Результаты пробных ЕНТ |
exam_score_pct | gte, lte | Результат выбранных ЕНТ 0…100% (values + value) |
learning_path_started, learning_path_completed | in, not_in | Начали / завершили траекторию |
learning_path_course_unlocked | in, not_in | Открыли выбранный курс внутри траектории |
points_total | gte, lte | Суммарный баланс баллов |
rank | gte, lte | Место в общем рейтинге; lte 10 = топ-10 |
last_active_at | before, after | Активность до/после RFC3339 |
push_enabled, has_device | eq | Значение "true" или "false" |
match=all— ученик должен соответствовать всем правилам;match=any— хотя бы одному.- Пустой
rulesозначает всех активных учеников текущей школы (с curator scope, если вызвал куратор). - Options endpoint отдаёт только минимальные
id,label,subtitleи применяет тот же scope — UI не запрашивает чужие domain endpoints. - Статусы кампании:
scheduled,processing,completed,cancelled,failed. Отмена не отзывает уже доставленные сообщения.
Шаблоны и ограничения payload
- Заголовок кампании или event template — не более 160 байт UTF-8; body — не более 2000 байт UTF-8. Счётчик UI показывает байты, а не количество JavaScript-символов.
- Итоговое сериализованное FCM-сообщение должно быть не больше 4096 байт. Превышение после подстановки переменных считается terminal delivery error и не ретраится.
deep_link— только same-origin относительный путь, начинающийся с одного/; scheme, host,//, backslash и управляющие символы запрещены.- FCM отправляется data-only, чтобы Service Worker показал ровно одно уведомление.
notification_idиспользуется как browser notification tag.
Конфигурация веб-клиента
NEXT_PUBLIC_FIREBASE_API_KEY=...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=...
NEXT_PUBLIC_FIREBASE_PROJECT_ID=...
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=...
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=...
NEXT_PUBLIC_FIREBASE_APP_ID=...
NEXT_PUBLIC_FIREBASE_VAPID_KEY=...Firebase client config и public VAPID key не являются секретами. Service Account credentials и путь PUSH_TOKEN_KEY_FILE к 32-byte ключу относятся только к backend/worker и никогда не должны попадать в NEXT_PUBLIC_*.