Тренажёр внимания и периферийного зрения: таблицы генерирует сервер, каждая попытка сохраняется вместе с покадровым разбором нажатий, статистика считается в Postgres.
client/ Vue 3 + Vite + Tailwind 4 — гексагональная архитектура (порты и адаптеры)
server/ NestJS + Prisma + Postgres — генерация таблиц, подсчёт результатов, агрегаты
docker compose up -d # Postgres на localhost:5433
cd server
cp .env.example .env # при необходимости поправьте DATABASE_URL/PORT
npm install
npx prisma migrate dev # схема + Prisma Client
npm run dev # API на http://localhost:3333/api
cd ../client
npm install
npm run dev # http://localhost:5173, /api проксируется на серверЕсли бэкенд не поднят, приложение само переключается на локальный адаптер: играть можно,
результаты лягут в localStorage, в шапке появится значок «офлайн».
Регистрации нет. При первом запросе сервер заводит анонимного пользователя и кладёт его id
в httpOnly-cookie (schulte_uid, год жизни). Все сессии и статистика привязаны к ней.
Имя для общего рейтинга задаётся на странице статистики.
| Режим | Что ищем | Максимальный размер |
|---|---|---|
CLASSIC |
числа 1…N² по возрастанию | 9×9 |
REVERSE |
числа N²…1 по убыванию | 9×9 |
GORBOV |
чёрные вверх и красные вниз попеременно | 9×9 |
LETTERS |
буквы русского алфавита | 5×5 (длина алфавита) |
Плюс режим фиксации взгляда: точка в центре, таблица проходится «боковым зрением».
Все ответы — JSON, cookie обязательна (credentials: 'include').
| Метод | Путь | Назначение |
|---|---|---|
GET |
/api/me |
профиль анонимной сессии |
PATCH |
/api/me |
имя для рейтинга |
GET |
/api/games/catalog |
режимы и допустимые размеры |
POST |
/api/games |
старт: сервер генерирует сетку и запоминает время |
GET |
/api/games/:id |
сессия с разбором нажатий |
POST |
/api/games/:id/finish |
нажатия игрока → итог считает сервер |
POST |
/api/games/:id/abandon |
пометить сессию брошенной |
GET |
/api/stats/summary |
сводка + разбивка по режимам и размерам |
GET |
/api/stats/sessions |
история попыток с пагинацией |
GET |
/api/stats/progress |
точки для графика + рекорд на каждый момент |
GET |
/api/stats/daily |
активность по дням и текущая серия |
GET |
/api/stats/leaderboard |
лучшие результаты по таблице |
DELETE |
/api/stats |
очистить свою статистику |
Клиент присылает только факты — { cellId, atMs }. Верное нажатие, точность, реакции и
итоговое время считает сервер, сверяя длительность со своими часами.
Не FSD, а порты и адаптеры: правила игры не знают ни про Vue, ни про сеть.
src/
domain/ правила: SessionRun, генерация сетки, подсчёт итога, агрегации
application/ порты (GameGateway, StatsGateway, ProfileGateway) и сценарии
infrastructure/ адаптеры портов: http/ (NestJS) и local/ (localStorage)
presentation/ Vue: страницы, компоненты, composables, форматтеры
app/ сборка приложения: DI-контейнер, роутер, стили
app/providers/container.ts подставляет в сценарии тот адаптер, который сейчас активен,
через прокси — поэтому переключение «сервер ↔ офлайн» происходит на лету и не требует
менять ни одну строчку в домене или в компонентах.
User → GameSession → Tap. В сессии лежит сгенерированная сетка и правильный порядок
обхода, поэтому сервер может пересчитать результат независимо от клиента, а Tap хранит
каждое нажатие с реакцией — из них строится разбор прохождения.
Всё живёт на бесплатных тарифах: статика и API — на Vercel, база — на Neon.
API собран одной serverless-функцией на том же домене, что и фронт, поэтому
httpOnly-cookie работает без sameSite=none и CORS не участвует.
api/server.js точка входа Vercel: реэкспорт server/dist/serverless.js
vercel.json сборка обеих частей и маршруты: /api/** на функцию,
остальное — на index.html для history-режима vue-router
Маршрут /api/** задан правилом в vercel.json явно: catch-all по имени файла
(api/[...path].js) на Vercel ловил только один сегмент пути, и всё вида
/api/games/catalog отбрасывалось с NOT_FOUND, не дойдя до функции.
Создать проект, в Connection Details взять две строки подключения:
| Переменная | Какую строку брать |
|---|---|
DATABASE_URL |
pooled — с -pooler в хосте |
DIRECT_URL |
direct — без -pooler |
Пул нужен потому, что каждая холодная функция открывает своё соединение; миграции же через пул не проходят и идут по прямому адресу.
- Add New Project → репозиторий, Root Directory оставить корнем.
- Environment Variables:
DATABASE_URL,DIRECT_URLиCOOKIE_SECURE=true. - Deploy.
vercel-build в server/package.json прогоняет prisma generate,
prisma migrate deploy и nest build — миграции накатываются на каждый деплой,
отдельного шага не требуется.
Первый запрос после простоя ждёт холодного старта функции (~1-2 с), Neon дополнительно будит базу (~0.5 с). Дальше всё отвечает как обычно.