Уведомления: persistent + live (push-to-invalidate → pull)
Новая система уведомлений поверх готовой SSE-шины: события пишутся в БД,
живут 72 часа и чистятся (при чтении списка + фоновой задачей), всплывают
сверху экрана в момент прихода, доступны через колокольчик в правом верхнем
углу (бейдж непрочитанных + панель).
Типы: приглашение в группу (→ /group), старт/финиш партии участникам кроме
инициатора (→ /match/{id}). Титулы — готовый хелпер-задел (не подключён, т.к.
выдача титулов игрокам ещё не реализована).
Бэкенд: модель Notification + миграция 0008 (идемпотентная), notification_service,
notify.notifications_changed, роутер /api/notifications (GET + /read), триггеры
в groups/matches, фоновая чистка в lifespan, защита hub.publish от закрытого loop.
Фронт: useNotifications/useMarkNotificationsRead, NotificationBell/Panel/Toaster,
перекомпоновка top-bar, стили; useServerEvents знает тип notifications.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -336,6 +336,33 @@ class MatchAttachment(SQLModel, table=True):
|
||||
created_at: datetime = Field(default_factory=_utcnow, nullable=False)
|
||||
|
||||
|
||||
# ─── Уведомления ─────────────────────────────────────────────────────────────
|
||||
|
||||
class Notification(SQLModel, table=True):
|
||||
"""Персистентное уведомление игроку (приглашение, старт/финиш партии, титул и т.д.).
|
||||
|
||||
Текст (`title`/`body`, RU) и ссылку (`link` — относительный SPA-путь) рендерит сервер —
|
||||
фронт лишь отображает. Хранятся 72 часа; протухшие чистятся фоном и при чтении списка."""
|
||||
|
||||
__tablename__ = "notifications"
|
||||
__table_args__ = (
|
||||
Index("ix_notifications_user_created", "user_id", "created_at"),
|
||||
)
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(
|
||||
sa_column=Column(
|
||||
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
)
|
||||
type: str = Field(sa_column=Column(String(32), nullable=False))
|
||||
title: str = Field(sa_column=Column(String(255), nullable=False))
|
||||
body: str | None = Field(default=None, sa_column=Column(Text, nullable=True))
|
||||
link: str | None = Field(default=None, sa_column=Column(String(255), nullable=True))
|
||||
read_at: datetime | None = Field(default=None, sa_column=Column(DateTime, nullable=True))
|
||||
created_at: datetime = Field(default_factory=_utcnow, nullable=False, index=True)
|
||||
|
||||
|
||||
# ─── Журнал аудита ───────────────────────────────────────────────────────────
|
||||
|
||||
class AuditLog(SQLModel, table=True):
|
||||
|
||||
Reference in New Issue
Block a user