Результаты (места и причина победы) теперь пишутся только в завершённую партию.
Раньше их можно было проставить идущей: партия оставалась in_progress, висела в
«Незавершённых», в статистику не попадала и очков не приносила — победитель есть,
а игры как бы не было. Дату и общий комментарий по ходу партии править по-прежнему
можно: двойственного состояния они не создают.
Форма завершения получила общий черновик (match_finish_drafts): раскладка мест,
ничьи, выбывшие, комментарии и причина победы видны всем, кто заполняет партию.
Отдельная таблица, а не колонки в matches, намеренно — запись в строку партии
дёргает onupdate у updated_at, то есть версию для оптимистичной блокировки, и
«Завершить» у второго участника ловил бы STALE_WRITE на каждую чужую правку.
Черновик удаляется при завершении и уходит каскадом при удалении партии.
Черновик разъезжается отдельным типом SSE-события: он меняется на каждое движение
тайла, и полная инвалидация (лидерборд, история, профили) по нему была бы
расточительной. Автору правки событие не шлётся.
#28
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
- IP клиента для аудита брался инлайном в 19 местах шести модулей; теперь
security.client_ip — за привратником адрес придётся читать из
X-Forwarded-For, и одна точка правки для этого обязательна.
- Чтение загруженной картинки (лимит размера + sniff формата) было скопировано
в четыре обработчика; вынесено в user_service.read_capped_image.
- Лимит размера вложения жил двумя одинаковыми константами в игроцком и
админском роутере — перенесён к самим вложениям.
- update_nickname и set_active_group переиспользуют nickname_format_ok и
group_service.get_membership вместо собственных копий проверки.
- Убраны осиротевшие импорты и комментарий-заготовка о вложениях, которые
давно реализованы (MatchAttachment).
#8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
Проход /simplify по backend/app:
- Три источника «текущего времени» (_utcnow в models.py и match_service.py при
живом timeutil.utcnow) сведены к одному — это прямо инвариант из CLAUDE.md.
- Бамп версии партии из двух независимых мест собран в match_service.touch:
следующая точка мутации, не трогающая строку matches, теперь имеет очевидный
способ сделать правильно.
- membership_service переиспользует group_service.get_membership вместо трёх
копий одного запроса; защита последнего владельца — один хелпер на удаление
и смену роли вместо двух похожих блоков.
- Убраны N+1: участники страницы партий и ники пригласивших берутся одним
запросом вместо запроса на строку (20 партий = 20 лишних запросов с двумя
join каждый).
- Счётчики партий считает СУБД (COUNT/MAX) вместо выгрузки всех строк ради
len() и max() в Python.
#8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
Выборки партий по игроку в бэкенде не было — только по группе. Добавлен
stats_service.user_match_list: завершённые партии игрока, свежие сверху;
сборка элементов вынесена из group_match_list в общий _match_items, чтобы
не дублировать её в двух местах.
Витрина профиля задаётся двумя колонками в users (миграция 0011):
history_mode (all/best) и history_detail (compact/full). Режим применяется
на бэкенде, а не на клиенте: это витрина владельца, и в том же виде
профиль видят гости. В режиме best берётся партия с максимальными League
Points из SCORED_CTE (при равных очках — более свежая).
GET /api/users/{user_id}/matches отдаёт список вместе с mode и detail —
гостю хватает одного запроса, чтобы отрисовать историю как задумал владелец.
#1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Раньше «любимая» вычислялась как самая частая среди партий с ручным
выбором фракции. Теперь это поле профиля: users.favorite_faction_id
(FK на factions, ON DELETE SET NULL, миграция 0010) и необязательное
поле в PATCH /api/users/me/profile.
Роутер профиля переведён на model_dump(exclude_unset=True) и обновляет
только пришедшие поля — иначе сохранение одного «о себе» обнуляло бы
выбранную фракцию.
«Основная фракция» логику не меняет (она и так считалась по всем партиям,
включая рандомные раздачи) и переезжает в подпись «Чаще всего играет на»:
FactionStat получает name_ru_prepositional из словаря предложного падежа
в faction_service. Словарь в коде, а не в БД: набор фракций задан игрой;
фракция вне словаря выводится в именительном.
Служебное chosen_games существовало только ради старой «любимой» — убрано
из SQL вместе с ней.
#17
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ps52xzuUXnWrJ5SRnEeaLk
Раньше уведомления удалялись через 72 часа после создания независимо от
прочтения: кто не зашёл за 3 дня, не видел их вовсе. Теперь по времени
чистятся только прочитанные (72ч после прочтения), а от роста у
незаходящих защищает кап MAX_PER_USER=100 — при создании удаляются
старейшие сверх лимита. Механика доставки не менялась: запись в БД +
pull при заходе, SSE лишь ускоряет обновление открытых вкладок.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Выбор места при завершении партии получает опцию «✕ выбыл»; выбывшему
авто-проставляется общее последнее место (ничья последнего ранга), плюс флаг
eliminated для отрисовки красным крестиком. В результатах и списке партий
выбывшие показываются крестиком и идут в конец. Подсчёт очков не меняется
(используются обычные числовые места).
Бэкенд: модель MatchParticipant.eliminated + миграция 0009 (идемпотентная);
схемы (place стал необязательным, +eliminated); match_service (хелпер
_resolve_finish_places, finish/update); проброс eliminated в роутерах и stats.
Фронт: выбор места с крестиком в MatchDetailPage и AdminMatchEdit, отрисовка в
результатах и MatchList, стиль .place-badge.eliminated; регенерация schema.d.ts.
Тесты: завершение с выбывшим (последнее место, засчитан), все-выбывшие → 422.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Новая система уведомлений поверх готовой 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>