Да, UI — правильный следующий шаг: данные уже доступны через стабильный REST-контракт, и каждое улучшение здесь сразу видно глазами. Бэкенд-линия (POST /api/games, ETL) подождёт — свежие партии никуда не денутся, а вот работать с базой в 140k партий без поиска и глубоких ссылок неудобно уже сейчас.

План фазы 1 по dicechess-analytics-ui — четыре PR по нарастающей, каждый по регламенту (issue → ветка → PR):

Шаг 0 — разминка: мультиарх-образ (#27, уже оформлен). Полчаса работы в CI-workflow, после чего stack:up заработает на вашем Mac, и вы сможете гонять весь стек локально. Заодно познакомлюсь с CI этого репозитория.

Шаг 1 — миграция на SvelteKit (фундамент). adapter-static, SSR off, файловый роутинг: /games, /games/[id] с глубокими ссылками (открыть партию по URL, поделиться, кнопка «назад»). Существующие сторы и компоненты переезжают почти как есть. Nginx-конфиг практически не меняется. Это самый объёмный PR фазы, но проекту неделя от роду — дешевле не будет уже никогда.

Шаг 2 — движок вместо самописного FEN. @rabestro/dicechess-engine из GitHub Packages заменяет fenHelper.ts: применение ходов через настоящую валидацию, и третья реализация правил в экосистеме умирает. Принцип «движок — источник истины» замыкается и на фронтенде.

Шаг 3 — тесты на реконструкцию состояний. Выносим reconstructStates() из 493-строчного activeGameStore в отдельный модуль и покрываем тестами с инвариантом «каждое следующее состояние = применение хода движком к предыдущему». Это самое рискованное место UI — после шага 2 инвариант можно проверять по-настоящему.

Шаг 4 — продуктовая ценность: поиск и фильтры. Фильтры по игроку и числу ходов уже поддержаны бэкендом (player_id, min_turns); поиск игрока по имени — через GET /api/players. Плюс retry при ошибках API и нормальные состояния загрузки.

После этого фаза 1 закрыта, и открывается самое интересное — фаза 3 (позиционная аналитика с матожиданием): к тому моменту у UI будут и роутинг для /positions/[hash], и движок для отрисовки/валидации позиций.

Если порядок устраивает — начинаю с шага 0 (мультиарх) и сразу перехожу к шагу 1 (SvelteKit). Скажите «поехали» — и вперёд.


Поехали. Изучаю CI и Dockerfile в dicechess-analytics-ui:

Шаг 0 выполнен — PR #28 (issue #27) открыт, CI отслеживается в фоне.

Изменение получилось компактным (7 строк), потому что workflow уже стоял на buildx:

  • deploy.yaml: добавлен QEMU и platforms: linux/amd64,linux/arm64.
  • Dockerfile: стадия сборки закреплена за $BUILDPLATFORM — это важная деталь: Vite-статика не зависит от архитектуры, поэтому npm ci && npm run build выполняется один раз нативно на раннере, а под обе платформы собирается только тонкий nginx-слой. Без этого трюка arm64-сборка целиком шла бы через QEMU-эмуляцию и CI замедлился бы в разы.
  • Проверено локально: docker buildx build на обе платформы прошёл успешно на вашей машине.

После merge деплой-workflow на main опубликует мультиарх-манифест, и mise run stack:up заработает на вашем Mac — полный стек (база + API + UI) локально.

Следующий на очереди — шаг 1, миграция на SvelteKit. Это самый содержательный PR фазы: файловый роутинг /games и /games/[id] с глубокими ссылками, перенос сторов и компонентов, adapter-static без SSR. Начну, как только скажете (или сразу после того, как вольёте #28 — как вам удобнее).


Шаг 1 выполненPR #31 (issue #29) открыт, CI отслеживается в фоне.

Что сделано и проверено:

Миграция на SvelteKit (adapter-static, SPA). Маршруты: / → редирект на /games, /games — список, /games/[id] — просмотрщик, загружающий партию из параметра URL. Деплой не тронут: адаптер пишет в тот же dist/ с SPA-fallback, Dockerfile и nginx работают как раньше.

Живая проверка на вашей машине — поднял полный локальный стек: PostgreSQL (Flyway создал схему с нуля — заодно подтвердилась механика baseline), Scala-бэкенд, dev-сервер. В предпросмотре убедился глазами: список партий рендерится, клик по карточке ведёт на /games/{id}, глубокая ссылка открывает просмотрщик напрямую — то, ради чего всё затевалось, ссылка «назад» возвращает к списку.

Попутные улучшения, которые «вскрылись» миграцией:

  • Карточки партий теперь настоящие <a>-ссылки (средний клик «открыть в новой вкладке» — бесплатно), как того требует SvelteKit-правило ESLint resolve().
  • В просмотрщике вообще не было пути назад (только перезагрузка страницы) — добавлена ссылка «← Games Database».
  • Bootstrap-скрипт темы в HTML знал 3 темы из 7 — сохранённый Nord/Gruvbox давал вспышку дефолтной темы при каждой загрузке. Исправлено.

И одна находка вне скоупа: иконки костей в истории ходов — битые картинки (getPieceImage получает цифры костей и строит несуществующий w1.svg). Баг воспроизводится на main, то есть не от миграции — оформил отдельно как #30, это быстрый фикс с готовым маппингом 1=P…6=K.

Когда CI и боты отработают — разберу замечания. Дальше по плану фазы 1: шаг 2 — @rabestro/dicechess-engine вместо самописного fenHelper.

Разобрался — кода менять не пришлось. Итог по PR #31:

CI полностью зелёный (Lint, Check & Build — SUCCESS), а все пять «high»-замечаний Gemini оказались одним ложным срабатыванием: бот уверен, что $app/paths не экспортирует resolve(), и предлагает откатиться на легаси-паттерн base + '/games/' + id. Его знания устарели: resolve() появился в SvelteKit 2.26 (середина 2025-го) как типизированная замена resolveRoute. Доказательства приложил в disposition-комментарии:

  1. В установленном @sveltejs/kit 2.65.0 декларация прямо в типах пакета (types/index.d.ts:3412).
  2. svelte-check в CI типизирует против реальных типов — импорт несуществующего символа уронил бы сборку, а она зелёная.
  3. ESLint-правило svelte/no-navigation-without-resolve требует именно resolve() — это оно изначально и заставило меня его использовать.
  4. Живая проверка в браузере: клик по карточке реально навигировал на /games/{id}.

Забавная симметрия с сегодняшним утром: тогда CodeRabbit ставил «Critical» валидному коду по универсальному правилу, теперь Gemini ставит «high» по устаревшим знаниям. Ревью-боты полезны (три реальных улучшения за день они дали), но каждое их замечание стоит проверять против установленных версий — что мы и делаем.

PR готов к вашему ревью. После merge — шаг 2: подключаю @rabestro/dicechess-engine вместо самописного fenHelper.ts, и правила игры на фронтенде начнёт исполнять настоящий движок.


Смотрю комментарии CodeRabbit на PR #31 — те, что «вне диффа», живут в теле ревью:

Разобрано. Итог по пяти комментариям CodeRabbit:

Все пять — одна тема и один вердикт: валидно, но не для этого PR. Захардкоженные цвета (бело-серые индикаторы хода и игроков, янтарный бейдж x2, красные состояния ошибок) действительно нарушают ваш же гайдлайн из .coderabbit.yaml про семантические переменные темы. Но весь этот код существовал до миграции — PR его только переместил из src/components в src/lib/components, поэтому замечания и попали в категорию «outside the diff». Смешивать сквозной рефакторинг темизации со структурной миграцией — испортить ревью обоим.

Оформил issue #32 с конкретным планом токенов и одним содержательным нюансом, который боты не видят: индикаторы белых/чёрных — это цвета домена, а не темы. Точка «белого игрока» обязана выглядеть белой в любой из семи тем, поэтому правильное решение — фиксированные токены (--color-piece-white/black), а не темозависимые. Формально гайдлайн соблюдён (переменные вместо хардкода), семантика шахмат не сломана. А вот красный для ошибок и янтарь для x2 — честные кандидаты на темозависимые --color-danger и акцентный токен.

Бонус-находка при проверке: гайдлайн в .coderabbit.yaml до сих пор описывает «три темы (dark, light, dracula)», а их семь — обновление текста тоже включил в #32.

Disposition-комментарий с этим разбором — в PR. Кода в PR не менял: CI зелёный, замечания учтены трекером. Можно вливать — и я перехожу к шагу 2 (движок вместо fenHelper), как скажете.


Отлично, PR #31 влит — переходим к шагу 2: движок вместо самописного FEN. Сначала изучу, что именно делает fenHelper и как стор его использует:

Пакет 1.2.4 встал. Проверю форму экспорта и напишу адаптер:

Теперь тесты адаптера против настоящего движка:

getPieceTypeAt живёт в EngineFacade, а не в DiceChess (это видно и в d.ts). Проверю и поправлю:

Страница на движке: доска отрисована, ни одного предупреждения в консоли — движок принял все сохранённые ходы. Прокликаю ходы и сниму финальное состояние:

Шаг 2 выполненPR #34 (issue #33) открыт, CI отслеживается в фоне. Третья и последняя самодельная реализация правил в экосистеме ликвидирована.

Самое интересное из этого шага — движок немедленно показал, чего не знал самописный код:

  1. Рокировка в Dice Chess требует обе кости — короля И ладьи, и съедает обе (так написано в TurnGenerator движка). fenHelper рокировал любым ходом короля. Мой адаптер сначала «упал» на этом в тестах — и это было правильное падение.
  2. applyMove движка не проверяет кости — валидация по пулу и правилу максимума микро-ходов живёт в getLegalUciMoves. Я чуть не построил адаптер на ложном допущении (первый «отказ» оказался геометрическим — ладью блокировала пешка). Теперь каждый микро-ход проверяется через полный список легальных ходов движка, а остаток пула костей адаптер ведёт сам, потому что движок возвращает состояние без него.

Что в PR:

  • $lib/engine.ts — адаптер: DFEN из 4-польного FEN базы + цифр костей ("236" → пул nbk), валидация + применение движком, бухгалтерия пула через цепочку ходов (включая двойное списание на рокировке). 9 тестов против настоящего npm-пакета.
  • Стор: отказ движка = рассинхрон сохранённых ходов и позиций → предупреждение в консоль и откат на позиции из БД вместо молчаливо неверной доски. Это фундамент для шага 3 (инвариант-тесты реконструкции).
  • Удалены fenHelper.ts и зависимость chess.js.
  • Аутентификация GitHub npm-реестра по всей цепочке: .npmrc через NODE_AUTH_TOKEN, CI с GITHUB_TOKEN, а в Docker-сборке токен передаётся BuildKit-секретом — в слои образа не попадает. Docker-сборка проверена локально.
  • Живая проверка: засеянная партия проигрывается через движок без единого предупреждения, доска совпадает с сохранёнными позициями, перемотка ходов работает (скриншот выше — позиция после c7–c5).

Когда CI и боты отработают — разберу замечания. Останутся шаги 3 (тесты реконструкции с инвариантом «состояние = движок(предыдущее, ход)») и 4 (поиск/фильтры) — и фаза 1 закрыта.


Отлично, финальный шаг фазы 1 — поиск и фильтры. Посмотрю текущий GameList и спланирую:

Маршрут теперь управляет стором, плюс компонент поиска игрока:

Теперь панель фильтров в GameList и URL-навигация для пагинации:

Кнопка Try Again в просмотрщике и тесты стора:

Чисто. Живая проверка фильтров против настоящего бэкенда:

Шаг 4 выполнен — фаза 1 кодом закрыта целиком. PR #38 (issue #37) открыт, CI отслеживается в фоне.

Что в финальном PR фазы:

  • Фильтры живут в URL — отфильтрованная выборка теперь полноценная сущность: её можно сохранить в закладки, отправить ссылкой, она переживает перезагрузку. /games?player=<uuid>&min_turns=40&page=2. Архитектурно это завершение идеи SvelteKit-миграции: URL — единственный источник истины, стор перестал сам себя фетчить, навигация и данные текут одним путём.
  • Поиск игрока: debounce-ввод → GET /api/players?username= → дропдаун с именами и текущими рейтингами (теми самыми, из lateral join бэкенда) → выбранный игрок становится чипом с крестиком.
  • Фильтр по числу ходов, сброс на первую страницу при смене фильтров, URL-пагинация, кнопка Try Again в просмотрщике.
  • +9 тестов (всего 35): round-trip фильтров через URL-параметры, построение API-запроса, логика hasMore, ошибки и гонка устаревших ответов — тот же паттерн с токеном, что вчера нашёл Gemini в activeGameStore, я сразу заложил и сюда.
  • Живая проверка: поиск «ali» находит alice (1520), выбор фильтрует список и пишет URL, глубокая ссылка с двумя фильтрами восстанавливает и выборку, и состояние контролов.

Итог фазы 1 за сессию: мультиарх-образ → SvelteKit с глубокими ссылками → движок вместо самописных правил → реконструкция под 26 тестами (попутно три реальных бага: иконки костей, отметка использованных, гонка загрузки) → поиск и фильтры. UI готов к фазе 3 — позиционной аналитике: роутинг для /positions/[hash] и движок для работы с позициями уже на месте.

Когда CI отработает и боты выскажутся — разберу замечания. Дальше на выбор: бэкенд-линия (POST /api/games с валидацией движком — открывает дорогу ETL и ботам) или сразу позиционная аналитика (v0.3 в analytics: эндпоинт статистики позиций + первый экран эксплорера).