Да, 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-правило ESLintresolve(). - В просмотрщике вообще не было пути назад (только перезагрузка страницы) — добавлена ссылка «← 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-комментарии:
- В установленном
@sveltejs/kit2.65.0 декларация прямо в типах пакета (types/index.d.ts:3412). - svelte-check в CI типизирует против реальных типов — импорт несуществующего символа уронил бы сборку, а она зелёная.
- ESLint-правило
svelte/no-navigation-without-resolveтребует именноresolve()— это оно изначально и заставило меня его использовать. - Живая проверка в браузере: клик по карточке реально навигировал на
/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 отслеживается в фоне. Третья и последняя самодельная реализация правил в экосистеме ликвидирована.
Самое интересное из этого шага — движок немедленно показал, чего не знал самописный код:
- Рокировка в Dice Chess требует обе кости — короля И ладьи, и съедает обе (так написано в
TurnGeneratorдвижка). fenHelper рокировал любым ходом короля. Мой адаптер сначала «упал» на этом в тестах — и это было правильное падение. 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: эндпоинт статистики позиций + первый экран эксплорера).