Commit Graph
48 Commits
Author SHA1 Message Date
NotBigGhost 101c457f7e удаление test-контура (дополнение) 2026-09-14 20:50:40 +03:00
NotBigGhostandClaude Opus 4.8 ce7fbad58e API: закрыть схему в проде + security-заголовки на edge
main.py: openapi.json/docs/redoc отдаются только в dev/test (нужны для gen:api),
в production отключены. Caddyfile (edge): HSTS, X-Content-Type-Options, X-Frame-Options,
Referrer-Policy, Permissions-Policy, скрыт Server; CSP подготовлена, но выключена до
проверки на test-клоне (строгая политика ломает SPA/Telegram-виджет/SSE). #61

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 17:19:42 +03:00
NotBigGhostandClaude Opus 4.8 0799aee684 Конфиг: fail-fast на дефолтных секретах в production
model_validator в Settings: при APP_ENV=production приложение не стартует, если
SECRET_KEY дефолтный/короче 32 символов или (при включённом бутстрапе) ADMIN_PASSWORD
дефолтный/пустой. dev/test не затронуты — там дефолты остаются нормой. #59

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 17:18:08 +03:00
NotBigGhostandClaude Opus 4.8 112b583264 Регистрация: throttle по IP от спама аккаунтов
throttle_register ограничивает частоту POST /api/auth/register с одного IP
через тот же LoginThrottle (считаются все попытки). Enumeration ников через
409 не закрываем — ники и так публичны в топе (отмечено в отчёте). #62

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 17:16:49 +03:00
NotBigGhostandClaude Opus 4.8 038a788f99 Throttle: IP-независимый лимит на аккаунт + заметка об устойчивости
Вход игрока и админа получают лимит login-user/admin-login-user, не зависящий
от IP: ротация X-Forwarded-For (#58) больше не снимает защиту полностью.
Успешный вход сбрасывает счётчики аккаунта. В docstring ratelimit — про сброс
при рестарте и необходимость внешнего стора при нескольких воркерах. #60

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 17:15:04 +03:00
NotBigGhostandClaude Opus 4.8 706eeb0af1 Вход админа: защита от перебора (throttle)
admin_login проходит через LoginThrottle (пара IP+логин, IP и сам аккаунт),
как вход игрока; исчерпание лимита -> 429. Неудачные попытки пишутся в аудит
(без пароля). Пароль админа — прямой путь к полному контролю, лимиты строже. #56

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 17:12:40 +03:00
NotBigGhostandClaude Opus 4.8 3f8667b561 Отзыв JWT при выходе и смене пароля
logout отзывает предъявленный токен по jti (in-memory denylist до exp);
смена и сброс пароля инкрементят users.token_version (claim ver в JWT,
сверка в auth/deps) — все прежние сессии отзываются. Своё устройство при
смене пароля остаётся в сессии (перевыдача cookie). Миграция 0013. #57

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 17:09:57 +03:00
NotBigGhost 0cbee9d866 Merge pull request 'Добавление аутентификации логин/пароль (#24)' (#53) from issue-24-login-password into dev
Reviewed-on: #53
2026-09-13 15:36:50 +03:00
NotBigGhostandClaude Opus 5 c0e162df72 Ошибка валидации: 422 без эха тела запроса вместо 500
Если JSON-эндпойнт получал тело с Content-Type text/plain (так шлёт HTML-форма),
сервер отвечал 500. _validation_handler клал exc.errors() в details как есть, а в
input там лежало сырое тело типа bytes, которое JSONResponse не сериализует.
Затронуты все эндпойнты с телом, в том числе /api/auth/telegram и
/api/admin/auth/login. Запрос и раньше отклонялся, сессия не создавалась, но
вместо понятной 422 в логе появлялся трейсбек.

Тот же input возвращал клиенту тело запроса целиком. Неполная форма входа
отправляла пароль обратно в ответе 422, откуда он мог осесть в логах прокси и
инструментах отладки.

Теперь в details идут только type, loc и msg. input и ctx (там бывают объекты
исключений — та же проблема сериализации) в ответ не попадают. Конверт и код
VALIDATION_ERROR прежние, фронт details валидации не читает.

Тесты: text/plain на двух эндпойнтах даёт 422, пароль не возвращается, loc
по-прежнему указывает на поле. На старом коде первые два падают.

#52

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 15:02:00 +03:00
NotBigGhostandClaude Opus 5 c73b4519cb Пароль и привязка Telegram в профиле, пароль игрока из админки
PUT /api/users/me/password задаёт или меняет пароль. Первый раз текущий пароль
не нужен: так его задают аккаунты из Telegram и все, кто появился до паролей.
Если пароль уже есть, нужен текущий. Иначе оставленная открытой сессия позволила
бы отобрать аккаунт насовсем, поэтому подбор текущего тоже ограничен: 5 неудач
на аккаунт за 15 минут. Ошибка 403 WRONG_CURRENT_PASSWORD, а не 401, чтобы фронт
не принял её за истёкшую сессию.

POST /api/users/me/telegram привязывает Telegram к аккаунту, созданному по
паролю. Подпись виджета проверяется так же, как при входе, ник не меняется.
Связка пишется в auth_identity, как при регистрации через Telegram, поэтому
следующий вход через Telegram попадает в этот аккаунт. Telegram, привязанный к
другому аккаунту, даёт 409 TELEGRAM_TAKEN, повторная привязка — 409
TELEGRAM_ALREADY_LINKED.

PUT /api/admin/users/{id}/password — способ восстановить забытый пароль: почту
приложение не хранит. Работает только для игроков, пароль админа по-прежнему
задаётся в .env. В аудит пишется только факт смены, без пароля. Сборка
AdminUserRead вынесена в хелпер, в ответе появилось has_password.

#24

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 14:02:19 +03:00
NotBigGhostandClaude Opus 5 82cf7a3393 Вход и регистрация по логину и паролю
Игроки входили только через Telegram, а вход по нику без пароля (stub) есть лишь
в dev. Теперь основной вход во всех окружениях: POST /api/auth/register и
POST /api/auth/login, логин — это ник. Stub в прод не переносится: он пускает без
секрета и по-прежнему живёт только в dev. Новый код лежит в прод-модуле
auth/password.py и dev-модули не импортирует. Схема БД не меняется: колонка
password_hash и провайдер local есть с первой миграции, на них построен вход
админа.

Пароль от 8 символов и не длиннее 72 байт: дальше bcrypt 5 бросает ValueError.
Схема API режет тело длиннее 128 символов ещё до bcrypt. Игроком входит только
role='player', так что учётка админа не открывает сессию игрока, и наоборот.
Неизвестный логин и аккаунт без пароля сверяются с фиктивным хешем и получают ту
же 401 INVALID_CREDENTIALS: по ответу и его времени нельзя понять, есть ли логин.

От перебора — скользящее окно 15 минут в памяти процесса (рассчитано на один
воркер, как SSE-шина): 5 неудач на пару «IP + логин» и 20 на IP, дальше 429
TOO_MANY_ATTEMPTS с retry_after. Пока блок стоит, пароль не проверяется вовсе.
Успешный вход сбрасывает счётчик пары, но не IP.

В MeRead появилось has_password: по нему фронт попросит задать пароль тех, у
кого его нет. Метод password добавлен в /auth/config.

#24

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 13:57:10 +03:00
NotBigGhostandClaude Opus 5 cb3af484cd CSRF: перевыдавать токен, если сессия есть, а cookie нет
На проде любые изменения данных, включая выход из аккаунта, отвечали 403
CSRF_FAILED, хотя сайт оставался залогиненным. Cookie csrf_token одна на игрока
и админку: вход в админку перезаписывал её со сроком 8 часов, а сессия игрока
живёт 7 дней. Когда токен истекал, сервер выдавал новый только при входе, а войти
и выйти мешала та же проверка. Из этого состояния было не выбраться, кроме как
стереть cookie сайта руками.

Теперь CSRFMiddleware перевыдаёт токен на любом ответе /api, если запрос несёт
сессионную cookie без csrf_token, в том числе на самом отказе. SPA на загрузке
делает GET /api/users/me, поэтому пользователю хватает перезагрузить страницу.
Правится только стартовое сообщение ответа, тело идёт насквозь, и SSE-поток
не буферизуется. Срок токена теперь не короче самой долгой сессии, так что вход
в админку больше не укорачивает токен игрока.

Проверка double-submit не ослаблена: запрос с cookie, но без заголовка или с
чужим токеном по-прежнему получает 403, и cookie в этом случае не перевыдаётся.
Перевыданный токен из кросс-сайтового ответа атакующему ничего не даёт: прочитать
cookie может только JS того же origin.

Тесты закрепляют восстановление на GET и на отказе, срок после входа в админку,
прежнюю строгость проверки, отсутствие токена у анонимов и то, что middleware
не склеивает чанки потока. На старом коде четыре из них падают.

#50

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 13:14:14 +03:00
NotBigGhostandClaude Opus 5 bdd07e8103 SSE: событие партии несёт участников, клиент не дёргает лишних
Событие о партии рассылается всей группе, и клиент по нему инвалидировал историю
игр, публичные профили и личную статистику получателя. В группе из шести человек
любая партия двоих заставляла остальные четыре вкладки перезапрашивать свою
историю и открытый профиль, хотя у них ничего не изменилось.

Теперь событие несёт participant_ids. Общие витрины (карточка партии, списки и
статистика группы, топ и главная) обновляются у всех — рейтинг глобальный, чужая
партия действительно двигает топ. История, публичный профиль и личная статистика
обновляются только у тех, кто играл, и у зрителей их профилей.

При удалении партии участники собираются ДО удаления: каскад уносит их строки
вместе с партией, и собранный после список всегда был бы пустым. Это же
поведение закреплено тестом.

Событие без participant_ids (вкладка открыта до обновления сервера) обрабатывается
по-старому, широко: обновление бэкенда не ломает уже открытые страницы.

#33

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
2026-09-09 20:47:32 +03:00
NotBigGhostandClaude Opus 5 e0f1ed5fee Статистика: один проход по строкам игрока вместо четырёх
Главная гоняла тяжёлый SCORED_CTE пять раз: лидерборд, три запроса профиля (итог,
разбивка по фракциям, форма последних партий) и ещё раз итог для активной группы.
Четыре последних выбирали одни и те же строки одного игрока и отличались только
агрегацией, а стоимость CTE растёт с числом партий во всём приложении, а не в
группе игрока.

Теперь строки игрока тянутся одним запросом, а итог, разбивка и форма считаются из
них в Python. Фильтр по группе — фильтрация того же набора, поэтому блок активной
группы не стоит отдельного прохода. На главной осталось два прохода вместо пяти,
у профиля — один вместо трёх.

Формула сглаженного рейтинга получила Python-версию рядом с SQL-версией, на тех же
константах: в SQL она нужна лидерборду, где агрегация идёт по всем игрокам. Чтобы
две реализации не разъехались (как однажды вышло с кэш-бастером аватара), добавлен
тест, сверяющий цифры профиля с цифрами того же игрока в лидерборде.

Второй тест считает запросы с SCORED_CTE на главной: без него оптимизация тихо
отъедет назад при следующей правке витрин.

#32

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
2026-09-09 20:38:29 +03:00
NotBigGhostandClaude Opus 5 0d81901a63 Партия: правка истории и кнопка «Редактировать» у игрока
Правка завершённой партии проверяла состав теми же правилами, что и создание:
участник обязан состоять в группе сейчас, фракция — быть доступной сейчас. После
отключения дополнения партию, сыгранную на Тау, было уже не исправить, а после
удаления игрока из группы — любую партию с ним. Теперь то, что уже записано в
партии, проходит всегда, а новые игроки и фракции по-прежнему берутся только из
текущего состава: чинить историю можно, занести в неё постороннего — нет.
Создание партии не ослабло.

Править завершённую партию умел любой участник группы, но только через API —
кнопки не было, и на практике это мог сделать лишь админ через админку. Теперь у
блока «Результаты» есть «Редактировать», и правка идёт тем же перетаскиванием,
что и завершение: раскладка восстанавливается из сохранённых мест (одинаковое
место — ничья, выбывшие отдельно), рядом — фракции, причина победы и комментарии.
Сохранение шлёт версию партии, так что устаревшая правка отклоняется как раньше.

Фракции вынесены отдельным блоком, а не в PlaceEditor: он занят перетаскиванием,
и селекты внутри него — лишний риск. В списке доступных фракций к набору группы
добавляются те, что уже стоят в партии, — иначе фракцию из отключённого
дополнения нельзя было бы даже оставить как есть.

#29

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
2026-09-09 20:16:33 +03:00
NotBigGhostandClaude Opus 5 8be9a23c0c Партия: общий черновик завершения и запрет результатов до финиша
Результаты (места и причина победы) теперь пишутся только в завершённую партию.
Раньше их можно было проставить идущей: партия оставалась 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
2026-09-09 19:39:31 +03:00
NotBigGhostandClaude Opus 5 94e2c09952 Simplify: общие хелперы в роутерах
- 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
2026-09-09 18:48:29 +03:00
NotBigGhostandClaude Opus 5 8ac2757cd6 Simplify: единый источник времени, версии партии и запросов
Проход /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
2026-09-09 18:45:50 +03:00
NotBigGhostandClaude Opus 5 9d9e4a345a Ревью: безопасность и корректность в сервисах бэкенда
Находки прохода /code-review high по backend/app:

- achievement_service: slug из URL шёл в путь без проверки, из-за чего
  DELETE /api/admin/achievements/%2E%2E удалял rmtree'ом родительскую папку
  каталога ачивок (в проде это /data — БД, uploads, ачивки целиком).
- match_service/attachment_service: версия партии = updated_at, но onupdate
  срабатывает лишь при реальном UPDATE строки matches. Правка одних участников
  и работа с вложениями его не вызывали, и оптимистичная блокировка молча
  пропускала конкурентную запись — бампаем updated_at явно.
- admin_service: удаление группы с партиями упиралось в RESTRICT и уходило
  наружу голым 500; теперь понятная ошибка. Админское удаление партии не
  чистило файлы вложений с тома — они оставались навсегда.
- user_service: при повторной загрузке аватара с тем же расширением avatar_path
  не менялся, updated_at не двигался, и кэш-бастер оставлял старую картинку до
  часа. Плюс версия считалась из наивного времени как из локального и
  разъезжалась с лидербордом, где то же поле считает SQL.
- membership_service: единственный владелец мог разжаловать сам себя и группа
  оставалась без владельца навсегда.
- notification_service: mark_read не слал SSE-сигнал, и бейдж непрочитанных на
  других устройствах висел до перезагрузки.
- routers/admin: created_at после правки пользователя отдавался без смещения,
  и дата «создан» прыгала на часовой пояс до следующего обновления списка.

#8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186Fk74jkkszahEHSjBzTjD
2026-09-09 15:24:52 +03:00
NotBigGhostandClaude Opus 5 be80891449 Профиль: история партий игрока и настройки её витрины
Выборки партий по игроку в бэкенде не было — только по группе. Добавлен
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>
2026-09-07 19:56:06 +03:00
NotBigGhostandClaude Opus 5 60b22dd8df Профиль: любимая фракция — выбор игрока, а не вывод из статистики
Раньше «любимая» вычислялась как самая частая среди партий с ручным
выбором фракции. Теперь это поле профиля: 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
2026-09-07 16:17:43 +03:00
NotBigGhostandClaude Fable 5 9ce501dd11 Профиль: «Основная фракция» — самая игранная с учётом рандома
Под любимой (только ручной выбор) — основная: max по всем партиям,
включая рандомные раздачи. Есть у любого сыгравшего игрока.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:12:18 +03:00
NotBigGhostandClaude Fable 5 e7323cc3c4 Статистика: любимая фракция — только по сознательному выбору игрока
Партии, где фракция выпала рандомом (was_random), в счётчик любимой
не идут: рандомная раздача о предпочтениях не говорит. Если игрок ни
разу не выбирал фракцию сам — любимой нет вовсе. Лучшая/худшая фракции
и остальная статистика считаются по всем партиям, как раньше.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:16:28 +03:00
NotBigGhostandClaude Fable 5 376605f554 API: avatar_url у участников партии (для блоков нового редактора мест)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:02:10 +03:00
NotBigGhostandClaude Fable 5 5c3aa6ba69 Уведомления: непрочитанные не протухают — игрок увидит их при любом заходе
Раньше уведомления удалялись через 72 часа после создания независимо от
прочтения: кто не зашёл за 3 дня, не видел их вовсе. Теперь по времени
чистятся только прочитанные (72ч после прочтения), а от роста у
незаходящих защищает кап MAX_PER_USER=100 — при создании удаляются
старейшие сверх лимита. Механика доставки не менялась: запись в БД +
pull при заходе, SSE лишь ускоряет обновление открытых вкладок.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 04:08:00 +03:00
NotBigGhostandClaude Fable 5 f22ee1ec29 UI: выбор игрока при создании партии — выпадашка в стиле подсказок
Вместо нативного select — кнопка-триггер (аватар + ник) с выпадающим
списком всех игроков группы в стилистике подсказок инвайта; игроки,
уже выбранные в других строках, не предлагаются (заодно исключены
дубли). Для аватаров в списке MemberRead теперь отдаёт avatar_url.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 03:00:26 +03:00
NotBigGhostandClaude Fable 5 4a681430af UI: рейтинг новичков в карточке профиля — серебристый
ProfileStats теперь отдаёт min_games (порог «Новичков» из API, без
дублирования константы на фронте): пока сыграно меньше min_games партий,
«Очки (рейтинг)» в профиле показываются серебристым, как в топах;
золотой — только подтверждённый рейтинг.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 03:10:53 +03:00
NotBigGhostandClaude Fable 5 ebb368686d Рейтинг: байесовское сглаживание по опыту; новички — до 10 партий
Проблема: средний балл не учитывал число игр — новичок с 10 удачными
партиями обходил ветерана с сотнями стабильных.

Теперь score = (C·m + сумма очков) / (C + игр) × 100 при C=10, m=0.5:
к реальным партиям «дописываются» 10 виртуальных со средним 0.5, рейтинг
стартует около 50 и с опытом сходится к чистому среднему (формула IMDB).
Применено в топах и профиле; разбивка по фракциям осталась на чистом
среднем (служебная метрика «лучшая/худшая», сглаживание давило бы её к 50).
MIN_GAMES 5 -> 10 («Новички» во всех топах — до 10 партий; подписи берут
порог из API). Справка обновлена, добавлен тест (62 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 03:04:48 +03:00
NotBigGhostandClaude Fable 5 e5b7420bae API: поиск игроков по нику для автокомплита (GET /api/users/search)
До 10 подсказок по подстроке ника среди активных игроков (role=player).
Регистронезависимо и для кириллицы: SQLite LIKE/lower сворачивают регистр
только у ASCII, поэтому фильтрация/ранжирование в Python (casefold):
префиксные совпадения раньше подстрочных, внутри — по алфавиту.
Пустой q — пустой список. Тесты + регенерированный schema.d.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:22:27 +03:00
NotBigGhostandClaude Fable 5 ac296a8f12 Новички: серебряный рейтинг и порог 5 игр вместо 3
- рейтинг в блоках «Новички» (топ игроков и общая статистика) выводится
  серебряным (--silver, класс .lb-score.provisional), а не золотым;
- MIN_GAMES 3 -> 5 (scoring.py) — в ранжированный топ попадают с 5 игр;
  подпись «Новички (меньше N игр)» берёт порог из API, справка обновлена;
- pytest: 57 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:21:20 +03:00
NotBigGhost f56f9eb47b Правка кеширования данных сайта 2026-06-26 23:06:04 +03:00
NotBigGhostandClaude Opus 4.8 1438a17441 Партии: «выбывшие» игроки (красный крестик) = последнее место
Выбор места при завершении партии получает опцию «✕ выбыл»; выбывшему
авто-проставляется общее последнее место (ничья последнего ранга), плюс флаг
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>
2026-06-26 16:54:37 +03:00
NotBigGhost 8f4a58d33d фикс бага с дублем аккаунтов при логине по нику 2026-06-26 15:33:27 +03:00
NotBigGhostandClaude Opus 4.8 f4286ffa6c Уведомления: 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>
2026-06-26 15:16:40 +03:00
NotBigGhostandClaude Opus 4.8 ef5c242e16 SSE: CSRF-middleware на чистом ASGI (BaseHTTPMiddleware буферизует поток и ломает SSE) + тест CSRF
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:08:10 +03:00
NotBigGhostandClaude Opus 4.8 2500ca575b Реал-тайм через SSE (push-to-invalidate): живые приглашения/партии, авто-обновление без перезагрузки
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:52:41 +03:00
NotBigGhostandClaude Opus 4.8 16452f7bbe Партии: оптимистичная блокировка (version) — устаревшие правки/отмена отклоняются (фикс гонки завершить-vs-отменить)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:39:20 +03:00
NotBigGhostandClaude Opus 4.8 043595beff Админ: управление медиа партии (добавление/удаление в любой момент)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:38:06 +03:00
NotBigGhostandClaude Opus 4.8 8ac1b5acef Партии: фото в общем комментарии (до 10), загрузка при завершении
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:34:39 +03:00
NotBigGhostandClaude Opus 4.8 87c7f48a3e Группы: статистика игроков — все участники (несыгравшие отдельным блоком), строка кликабельна целиком
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:11:32 +03:00
NotBigGhostandClaude Opus 4.8 a5c54512a5 Группы: приглашения вместо авто-добавления (согласиться/отказаться)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:07:20 +03:00
NotBigGhostandClaude Opus 4.8 c4065962e2 Редактор ачивок в админке + файловое хранилище ачивок (задел под титулы)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:41:20 +03:00
NotBigGhostandClaude Opus 4.8 96daddae05 Кастомизация профиля (о себе, аватар) и просмотр профилей других игроков + общая статистика
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:27:24 +03:00
NotBigGhost b302ec769a переименовывание аккаунтов админом 2026-06-17 16:43:43 +03:00
NotBigGhost 3291c67b2c Добавлена функция удаления аккаунтов с каскадным удалением из всех записей для дева 2026-06-17 16:34:29 +03:00
NotBigGhost 865c967d5e Добавлена форма изменения ника при регистрации аккаунта на случай, когда уже существует аакунт с ником, совпадающим с ТГ тегом регистрируемого аккаунта 2026-06-17 16:17:14 +03:00
NotBigGhost b04fbb2e17 Завершена настройка домена для дева, теста и прода. Соединение через ssh-туннель 2026-06-16 19:15:54 +03:00
NotBigGhost 56b5d09a4d v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика 2026-06-16 17:41:06 +03:00