SPA для работы перевозчика с грузовыми аукционами по предоставленному OpenAPI-контракту. Проект последовательно строится на React, TypeScript, Vite, TanStack Router и Feature-Sliced Design.
Опубликованный стенд: https://maker27.github.io/freight-auctions/ — собирается
и публикуется автоматически из main после прохождения всех гейтов, содержит MSW
и закрыт от индексации. Пока репозиторий закрытый, адрес недоступен извне.
Посмотреть локально можно без установки зависимостей:
docker compose up --build # http://localhost:8080или той же сборкой стенда через Node:
npm ci && npm run build:stand && npm run previewПубличная витрина и каталог аукционов в светлой схеме, кабинет — в тёмной. Скриншоты сняты Playwright’ом с production-сборки стенда при 1440 × 940.
| Публичная витрина | Каталог с фильтрами |
|---|---|
![]() |
![]() |
| Детали аукциона | История ставок | Форма ставки |
|---|---|---|
![]() |
![]() |
![]() |
- каждый ответ API проверяется в рантайме Zod-схемами, сгенерированными из OpenAPI; отдельный гейт падает при дрейфе генерации;
- защищённые контакты, адреса, координаты и цена груза удаляются до попадания в TanStack Query cache, а не только скрываются в разметке; это закреплено тестами;
- 514 тестов в 70 файлах и 5 браузерных сценариев Playwright; порог покрытия по
слоям
libиmodel— гейт CI, а не справка; - доступность и валидность разметки проверяются автоматически:
vitest-axeиhtml-validateна отрендеренном DOM, отдельный тест контраста 13 утверждённых пар токенов в обеих схемах; - нефункциональные бюджеты считаются из Vite manifest и падают при превышении: 209,05 из 250 КиБ начального JS gzip, CSS 5,36 из 20 КиБ, шрифты 65,46 из 72 КиБ;
- матрица 9 × 9 статусов аукциона и участия с безопасным поведением для
неизвестных сочетаний; время подаётся через внедряемые часы, а не
Date.now(); - конкурентная ставка: принудительное перечитывание условий перед
POST, mutex двойной отправки, отсутствие ложного успеха при неопределённом сетевом исходе; - единый источник визуальных значений
tokens.ts→tokens.generated.cssс немутирующей проверкой дрейфа и запретом визуальных литералов в коде; - SPA работает и в корне домена, и в подкаталоге Pages: base path один для роутера, API и Service Worker;
- Docker с непривилегированным nginx, cache policy и healthcheck, публикация Pages и GHCR из того же workflow только после успешных гейтов.
- React, TypeScript в строгом режиме и Vite;
- TanStack Router и TanStack Query;
- React Hook Form и Zod;
- MSW и Zustand для точечного клиентского UI-state;
- Tailwind CSS, собственный
shared/uiна Radix, Tabler Icons и Sonner; - Vitest, React Testing Library,
vitest-axeиhtml-validate.
- Node.js
>=22.17.0 <23; - npm
10.9.8.
npm ci
npm run devЛокальный сервер по умолчанию открывается на http://localhost:5177. Порт для
dev и preview можно переопределить в локальном .env (пример есть в
.env.example):
DEV_SERVER_PORT=5180Допустимо целое значение от 1 до 65535. Некорректное значение или занятый порт останавливают запуск вместо тихого выбора другого адреса.
Production-сборка и локальный preview:
npm run build
npm run previewСборка для GitHub Pages под именем этого репозитория и её локальный preview:
npm run build:pages
npm run preview:pagesbuild:pages задаёт base path /freight-auctions/, включает MSW и режим
публичного демо, создаёт dist/404.html из того же app shell и закрывает весь
артефакт от индексации. Поэтому ассеты, Router, mock API, worker и прямое
открытие SPA-маршрутов используют единый подкаталог GitHub Pages.
Контейнерный стенд собирается и запускается одной командой:
docker compose up --buildПосле healthcheck приложение доступно на http://localhost:8080. Порт можно
переопределить, например FREIGHT_AUCTIONS_PORT=8090 docker compose up --build.
Образ содержит root-сборку build:stand с MSW и запретом индексации, nginx
работает непривилегированным пользователем и отдаёт SPA-маршруты через fallback
на index.html. HTML, CSS и JavaScript передаются с gzip; хешированные assets
кешируются на год как immutable, а app shell, Service Worker и SEO-файлы
ревалидируются через ETag и Last-Modified без сохранения устаревшей версии.
Остановка и удаление контейнера: docker compose down.
- публичная витрина с маршрутным SEO-слоем, JSON-LD и парной hero-иллюстрацией для светлой и тёмной схемы;
- локальная демо-сессия, guard закрытого контура и безопасное восстановление исходного URL вместе с query и hash;
- каталог аукционов: URL-фильтры с Zod-разбором, сортировка, размер страницы, пагинация router-ссылками, вид «список / сетка» и все async-состояния;
- детальная страница: все контрактные секции, четыре permission-флага, prefetch по фокусу и наведению, опрос активного аукциона раз в 15 секунд;
- история ставок: адаптивные таблица и мобильный список, сортировка с
aria-sort, победитель, отмена с причиной, скрытая история без запроса; - форма ставки: денежные ограничения в минорных единицах, помощники шага и
доступной цены, перечитывание условий перед
POST, обработка422и401; - переключатели цветовой схемы, режима отображения НДС и вида каталога;
- транспорт с рантайм-валидацией ответов, доменными ошибками, таймаутом и повторами только для читающих операций;
- доменный слой: брендированные идентификаторы, матрица статусов, состояния поля, внедряемые часы и московская трактовка дат без смещения;
- stateful mock API на MSW для четырёх операций с детерминированными сценариями прав, пустоты и ошибок;
- собственный UI-кит на нативных элементах и Radix, локальный шрифт Inter, единый источник токенов;
- Docker, Compose, nginx и единый CI-workflow с публикацией Pages и GHCR.
Подробный перечень по слоям — в docs/features.md.
После успешной ставки форма остаётся на своём прямом URL: активный аукцион допускает изменение ставки, поэтому пользователь сразу видит обновлённые ограничения и может осознанно изменить значение. Переход к деталям выполняется явной ссылкой и сохраняет точный URL исходного списка.
Вход полностью локальный: он не обращается к серверу, не проверяет реальные
учётные данные и не предоставляет настоящую авторизацию. Подходит любой
корректный email и пароль длиной не менее восьми символов. Значения формы не
сохраняются; в sessionStorage записывается только признак активной
демо-сессии.
Неавторизованный переход на любой URL ветки /auctions перенаправляет на
/login. После успешной отправки формы пользователь возвращается на исходный
внутренний URL. Внешние и некорректные redirect-значения заменяются безопасным
/auctions.
Корневой маршрут описывает возможности демонстрационного кабинета и не загружает данные аукционов. Canonical строится из текущего origin и Vite base path, поэтому одинаково работает в корне домена и в подкаталоге Pages.
Обычная production-сборка разрешает индексирование витрины и /login; оба
маршрута получают canonical, social metadata и JSON-LD, а вход включён в sitemap
как потенциальный sitelink «Личный кабинет». Ветка /auctions закрыта в
robots.txt и маршрутных метаданных. Режим build:pages задаёт
VITE_PUBLIC_DEMO=true: весь публичный контур получает
noindex, nofollow, а итоговый артефакт содержит статический robots meta и
robots.txt с Disallow: /. sitemap.xml содержит / и /login, но не
включает кабинет; полный запрет индексации стенда задаётся robots и route
metadata.
Hero-иллюстрация имеет отдельные светлую и тёмную версии и переключается по
фактически разрешённой теме, включая ручной выбор, отличный от системного.
Изображение дублирует контекст соседнего текста, поэтому остаётся декоративным
с пустым alt; браузер загружает только URL активной версии.
MSW включается автоматически до первого рендера React в development, в
build:stand и в build:pages демонстрационного стенда. Под корневым или
настроенным базовым путём он перехватывает четыре контрактных адреса api/v1:
POST /auctions/list;GET /auctions/{auctionUuid};GET /auctions/{auctionUuid}/bets;POST /auctions/{auctionUuid}/bets.
Обычная production-сборка по умолчанию ожидает настоящий API. Режим можно
задать явно через VITE_API_MODE=mock|remote; любое другое значение
останавливает приложение с ошибкой конфигурации. build:stand и build:pages
используют отдельный Vite mode из .env.pages и всегда собирают демонстрацию с
MSW. В Vitest тот же набор хендлеров подключён через Node server; состояние
сбрасывается после каждого теста.
Фильтр списка cargo_num: "MOCK-503" воспроизводимо возвращает 503, а
несуществующее значение обычного фильтра — пустой список. Остальные сценарии
прав и данных представлены отдельными аукционами со стабильными UUID в
src/mocks/data/auctions.fixture.ts.
public/mockServiceWorker.js — сгенерированный MSW CLI браузерный worker. Он
раздаётся отдельным файлом, потому что Service Worker должен иметь стабильный
URL и scope приложения. Файл совпадает с поставляемой версией MSW и вручную не
редактируется; директивы отключения линтеров являются частью upstream-файла.
| URL | Назначение | Индексация |
|---|---|---|
/ |
публичная демо-витрина | зависит от режима |
/login |
локальный демо-вход | зависит от режима |
/auctions |
список аукционов | noindex, nofollow |
/auctions/$auctionUuid |
детали аукциона | noindex, nofollow |
/auctions/$auctionUuid/bets |
история ставок | noindex, nofollow |
/auctions/$auctionUuid/bet |
создание или изменение ставки | noindex, nofollow |
При push в main единый workflow сначала выполняет все гейты и собирает
Pages-артефакт. Только после успешного job Verify этот артефакт публикуется в
GitHub Pages, а Dockerfile
собирается и образ отправляется в
GHCR
с тегами latest и полным SHA коммита. Для pull request выполняются гейты и
Pages-сборка, но внешняя публикация не запускается.
В настройках репозитория источником Pages должен быть выбран GitHub Actions.
Workflow использует только штатный GITHUB_TOKEN: секреты приложения для
демо-стенда не нужны. Опубликованный Pages-артефакт и контейнерная сборка всегда
включают MSW и noindex, nofollow; обычный npm run build по-прежнему ожидает
настоящий API и не подменяется демонстрационным режимом.
npm run env:check
npm run api:validate
npm run api:types:check
npm run typecheck
npm run lint
npm run format:check
npm run dead-code:check
npm run names:check
npm run tokens:check
npm run test:run
npm run test:coverage
npm run test:e2e
npm run build
npm run build:budget
npm run build:stand
npm run build:pages
docker compose config
nginx -t -c "$PWD/deploy/nginx.conf"test:e2e запускает браузерный смоук Playwright поверх production-сборки
build:stand с MSW: каталог с фильтром и сортировкой, форма ставки и проход
клавиатурой без указателя. Перед первым запуском нужен браузер:
npx playwright install chromium. Полное покрытие остаётся за интеграционными
тестами Vitest, смоук их не дублирует.
test:coverage прогоняет те же тесты и дополнительно требует порогов покрытия по
слоям lib и model: 85 % строк и операторов, 90 % функций, 75 % ветвлений. Этот
же скрипт выполняется в CI вместо test:run, поэтому порог является гейтом, а не
справкой.
Локальные хуки ставит husky через prepare. pre-commit запускает
lint-staged (eslint --fix и prettier --write по отобранным файлам) и
npm run typecheck; pre-push запускает npm run test:run. Хук не изменяет
индекс за пределами файлов, уже отобранных разработчиком. Полный набор гейтов
остаётся за CI и ручной проверкой: дублировать его в хуке нельзя, иначе хук
начнут обходить через --no-verify.
tokens:check сравнивает генерируемый CSS с tokens.ts, не изменяя рабочее
дерево. visual:check, входящий в lint, запрещает визуальные литералы,
произвольные значения Tailwind, inline-стили и динамически составленные классы.
format:check проверяет единое форматирование Prettier, а dead-code:check
ищет неиспользуемые файлы, зависимости и публичные экспорты. Для сгенерированных
OpenAPI-файлов и динамически загружаемого источника токенов действуют точечные
исключения только для неприменимых видов export-проверок.
Обычная remote production-сборка создаёт Vite manifest и автоматически запускает
build:budget: начальный JavaScript ограничен 250 КиБ gzip, дополнительные
чанки каждого lazy-маршрута — 120 КиБ gzip, весь CSS — 20 КиБ gzip, локальные
файлы шрифта — 72 КиБ gzip. Отдельная команда полезна для повторной проверки уже собранного dist.
Browser-only MSW из demo-сборок измеряется отдельно: это инфраструктура
демонстрационного backend, а не код remote production-приложения.
Генерация TypeScript-типов и Zod-схем из OpenAPI выполняется отдельно; проверка дрейфа повторяет генерацию и требует чистого результата:
npm run api:types
npm run api:types:checkПолный состав покрытого тестами поведения, проверки SEO и конфигурации поставки, а также матрица соответствия первичному заданию — в docs/verification.md.
Последний полный прогон — 03.08.2026: OpenAPI и generated drift, typecheck,
lint с visual gate, format:check, 514 тестов в 70 файлах, test:coverage
(строки 94,55 %, операторы 94,54 %, функции 98,93 %, ветвления 85,52 %),
5 браузерных сценариев Playwright, dead-code:check, names:check,
tokens:check, обычная production-сборка, root-сборка стенда, Pages-сборка и
бюджетный гейт. env:check на машине разработчика падает из-за Node 24.18.1 и
npm 11.16.0 вместо требуемых 22.17.0 и 10.9.8; сам репозиторий согласован, CI
берёт версию из .nvmrc.
Remote production укладывается в бюджеты: транзитивный начальный JavaScript — 209,05 КиБ gzip из 250 КиБ, дополнительные чанки lazy-маршрутов — от 2,55 до 5,51 КиБ из 120 КиБ, весь CSS — 5,36 КиБ из 20 КиБ, локальные шрифты — 65,46 КиБ из 72 КиБ. Browser-only MSW demo-сборок занимает 163,32 КиБ gzip и раскрывается отдельно от бюджета production-приложения.
Ручная браузерная матрица пройдена 01.08.2026 в headless Chromium на сборке с полным набором аукционных экранов:
- вход с клавиатуры, фокус первого невалидного поля и возврат на исходный URL;
- список с данными, пустым результатом, безопасным fallback URL, 503 после завершения retry, пагинацией и мобильной панелью фильтров;
- карточки и деталь со всеми секциями, not-found и вариантами скрытых контактов, адресов и цены груза;
- история с обычной, победившей и отменённой ставками, пустое и скрытое
состояния; при скрытой истории запрос
/betsне выполнялся; - форма ставки: пустое значение, успешная ставка 139 000 ₽, обновлённая доступная цена, единственный toast, сохранённый ввод и запрещённое состояние;
- ширины 360, 768 и 1280 px, светлая и тёмная схемы, отсутствие горизонтального
переполнения,
Escapeи возврат фокуса мобильной панели; - отсутствие скрытых контактов, адресов, координат и цены груза в DOM и TanStack Query cache.
В консоли страниц неожиданных warning/error и ошибок приложения не было. При намеренном воспроизведении not-found и 503 Chromium ожидаемо зарегистрировал сетевые ответы 404/503; UI обработал их предусмотренными состояниями. Процесс Snap Chromium отдельно вывел инфраструктурные сообщения GCM и software-GPU: они не относятся к приложению и в консоль страниц не попали. Проверка реальным скринридером и работа с настоящим backend не выполнялись.
Витрина и SEO проверены 02.08.2026 в headless Chromium на 360, 768 и 1280 px:
критичный текст и действия не обрезаны по горизонтали, маркировка демо заметна,
title, robots и canonical обновляются, неожиданных ошибок страницы нет. В
Pages-артефакте проверены статический noindex, nofollow, robots.txt с полным
Disallow: /, sitemap с двумя публичными URL и скомпилированный demo-режим
метаданных.
Поставка проверена 02.08.2026: docker compose config и синтаксис
deploy/nginx.conf через nginx -t прошли, для обоих закреплённых base images
подтверждён multi-platform manifest, root-артефакт проверен на MSW,
noindex, nofollow, 404.html и полный Disallow: /. Docker-образ собран,
контейнер достиг состояния healthy; прямой SPA-маршрут, заголовки noindex и
Cache-Control и ответ 304 на условный запрос проверены через nginx, после
чего контейнер и Compose-сеть удалены. Переопределение порта проверено реальными
запусками dev на 5197 и preview на 5198.
Не выполнялись: ручной браузерный проход после последних изменений интерфейса, проверка реальным скринридером, работа с настоящим backend и GitHub-hosted deploy jobs.
Направление зависимостей:
app -> pages -> widgets -> features -> entities -> shared
- TanStack Router владеет маршрутами, фильтрами и пагинацией в search params.
- React Hook Form владеет значениями форм входа, фильтров и ставки до их применения.
- Zustand хранит только локальную демо-сессию, UI-предпочтения и состояние мобильной панели фильтров.
- TanStack Query владеет серверным списком и деталями; кэш полностью очищается при выходе из демо-сессии.
- Нормализованный MSW store владеет только состоянием демонстрационного бэкенда; наружу возвращаются клоны контрактных DTO.
shared/uiсодержит доменно-независимые компоненты с публичным API каждого сегмента.
- Демо-сессия не является механизмом безопасности и не заменяет серверную авторизацию.
- GitHub-hosted jobs публикации и фактические URL стенда/GHCR станут проверяемы
только после коммита и
pushвmain; эта внешняя операция не выполнялась.




