feat: align docs with code, finish_place, registered status, UI filters, tests, CI
Some checks failed
CI / build-and-test (pull_request) Has been cancelled

- Add PLAN.md and sync backend docs, .env.example, API doc (404 details)
- Document mock DB and PORT/API_PORT in docs/backend.md; README monorepo + frontend/.env.example
- Migration 002: finish_place column, status registered; mapper and mock DB updated
- Frontend: registered status, finishPlace, calendar year/month filters, pace sparkline
- Extract createApp for tests; supertest + tsx; GitHub Actions CI

Made-with: Cursor
This commit is contained in:
Vaka.pro
2026-04-06 22:20:31 +03:00
parent 1ffc3a65eb
commit a2dcf67396
27 changed files with 1410 additions and 286 deletions

229
PLAN.md
View File

@@ -1,202 +1,61 @@
# План: календарь забегов (SPA + API + PostgreSQL)
# Calendar Run — план продукта
Консолидированный план реализации с актуальными решениями. После инициализации git — работать в отдельной ветке (например `feature/race-calendar-app`).
Монорепозиторий: **backend** (Express + PostgreSQL) и **frontend** (React + Vite). Цель — календарь стартов с метриками бегуна: планирование, результаты, PR и сравнение.
## 1. Цели продукта
## Вне объёма (намеренно)
1. **Расписание по месяцу** — выбор месяца/года, список забегов.
2. **Расписание на год** — календарная сетка года, отметки на датах со стартами; по клику на дату — модалка/панель со списком забегов и переход на карточку.
3. Добавлять забеги **запланированные** и **уже прошедшие**.
4. Для **прошедших** — ввод/редактирование **результата** и **стартового номера**; поля можно дописать позже.
5. **Личные рекорды** на главной по дистанциям: 1 км, 5 км, 10 км, 15 км, 21.1 км, 42.2 км — обновляются, если в забеге указан более быстрый результат на «подходящей» дистанции.
6. **Старты (организатор):** дата и время старта, расписание кластеров, выдача номеров — **ручной ввод**; обязательна возможность указать **официальную ссылку** на страницу организатора (автопарсинг сайтов не входит в объём).
7. **Авторизация не требуется.**
- Авторизация, мультипользовательность, личные кабинеты.
- Парсинг сайтов организаторов и автозагрузка результатов.
- Отдача статики SPA с того же процесса, что и API (фронт — отдельный Vite/build).
## 2. Исходные данные в репозитории
## Модель данных `Race` (API — camelCase)
- Файл `import/races_2026_calendar.csv` — колонки `date`, `month`, `day`, `event`, `distance_km`.
- Назначение CSV: **один раз** — как вход для **seed-скрипта**, который пишет строки в PostgreSQL.
- **В рантайме** ни фронт, ни API этот CSV **не читают**. После успешного seed файл может оставаться в репо только как архив/референс.
| Поле | Тип | Описание |
|------|-----|----------|
| `id` | string | Стабильный ключ, например `{YYYY-MM-DD}-{slug}` |
| `date` | string | `YYYY-MM-DD` |
| `title` | string | Название |
| `distanceKm` | number | Дистанция, км |
| `status` | `planned` \| `registered` \| `completed` \| null | Жизненный цикл старта |
| `officialUrl` | string \| null | Сайт организатора |
| `startTime` | string \| null | Время старта (строка, напр. `09:30`) |
| `clusterSchedule` | string \| null | Расписание кластеров |
| `bibPickup` | string \| null | Выдача номеров |
| `bibNumber` | string \| null | Стартовый номер |
| `finishTime` | string \| null | Результат `H:MM:SS` или `MM:SS` |
| `finishPlace` | string \| null | Место на финише (текст: «3», «3/120» и т.п.) |
| `notes` | string \| null | Заметки |
| `createdAt` | string | ISO, read-only |
| `updatedAt` | string \| null | ISO, read-only |
## 3. Стек и структура репозитория
PostgreSQL: `snake_case` столбцы, маппинг в [`backend/src/mappers/race.ts`](backend/src/mappers/race.ts).
- **Монорепозиторий:** `frontend/` (Vite + React 18 + TypeScript + react-router-dom) и `backend/` (Node.js + Fastify или Express).
- **БД:** PostgreSQL; схема — SQL-миграции в `backend/` (или согласованный каталог миграций).
- **Локально:** `docker-compose.yml` с сервисом Postgres.
- **Стили:** CSS + **BEM** + CSS variables (минимальная дизайн-система: цвета, отступы, типографика).
- **Даты/время:** `date-fns` или `Intl`, русская локаль там, где нужно.
## HTTP API (минимум)
### 3.1 Переменные окружения
- `GET /health` — liveness без БД.
- `GET /ready` — readiness (подключение к БД; в режиме mock считается доступной — только для dev/CI).
- `GET /races` — список; query: `year`, `month` (целые; `month` 112).
- `GET /races/:id`, `POST /races`, `PATCH /races/:id`, `DELETE /races/:id`.
**Только на сервере API (никогда не во фронт-бандле):**
Ошибки: JSON, единый стиль (`validation_error`, `not_found`, `conflict`, `database_unavailable`). Подробности — [`docs/backend-api-for-frontend.md`](docs/backend-api-for-frontend.md).
```env
DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
```
## Seed
Дополнительно по необходимости: `PORT` или `API_PORT`, `NODE_ENV`, `CORS_ORIGIN`.
- Файл [`import/races_2026_calendar.csv`](import/races_2026_calendar.csv).
- Стабильный `id`, upsert по `id`. Повторный запуск безопасен.
**Фронтенд (Vite):** только публичный адрес API, например:
## Режим без PostgreSQL (dev/CI)
```env
VITE_API_BASE_URL=http://localhost:3001
```
Переменная `CALENDAR_RUN_MOCK_DB=1` (или `true`): HTTP-обработчики используют заглушку пула **без** реальной БД. **Не использовать** для `npm run db:migrate` и `npm run seed` — нужен настоящий Postgres и `DB_*`.
В коде: `import.meta.env.VITE_API_BASE_URL`.
## Frontend (SPA)
Шаблон без секретов — корневой `.env.example`; описание — в `docs/frontend.md` и `docs/backend.md`.
- Маршруты: дашборд (`/`), список стартов (`/races`), карточка (`/races/:id`).
- Дашборд: ближайший старт, последний результат, PR, сезон, PR по ключевым дистанциям, сравнение завершённых стартов, при необходимости — лёгкая визуализация прогресса.
- Список: будущие / прошедшие; фильтрация по году и месяцу через API.
- Стили: BEM и дизайн-токены; ориентир по духу — [`agent-frontend-ui-instructions.md`](agent-frontend-ui-instructions.md).
## 4. Источник правды и поток данных
## Критерии готовности текущей итерации
### 4.1 Рантайм
Единственный источник правды для календаря и карточек в работающем приложении — **PostgreSQL**. SPA общается **только с HTTP API**.
**localStorage не используется** для хранения забегов или «дельт».
```mermaid
flowchart LR
User[User]
SPA[React SPA]
API[Node API]
DB[(PostgreSQL)]
User --> SPA
SPA -->|"fetch VITE_API_BASE_URL"| API
API -->|"DB_* connection"| DB
```
### 4.2 Вне рантайма (разово)
**Seed-скрипт** (запуск вручную при развёртывании/обновлении стартового набора):
- Читает `import/*.csv` и/или опционально `public/data/races.json`.
- Выполняет `INSERT` / upsert в таблицу `races`.
- Не вызывается из SPA и не выполняется на каждом HTTP-запросе.
Промежуточная ступень **CSV → races.json для работы SPA не обязательна**: seed может писать в БД напрямую из CSV и/или из JSON.
### 4.3 Разовый перенос CSV → БД
- Парсинг: заголовок; кавычки в `event`; `distance_km` — число; `date``YYYY-MM-DD`.
- После переноса приложение в обычном режиме **не использует** `import/races_2026_calendar.csv`. Новые старты — через UI/API (или отдельная админ-фича импорта, если появится позже).
## 5. Модель данных (БД и API)
Один набор полей — в таблице, в JSON тел запросов/ответов API и в опциональном файле для seed. Согласовать **camelCase в API** и **snake_case в SQL** в `docs/backend.md`.
| Поле | Обяз. | Примечание |
| ----------------- | ----- | ---------------------------------------------------- |
| `id` | да | Стабильный ключ, напр. `2026-05-03-kazan-marathon` |
| `date` | да | День старта `YYYY-MM-DD` |
| `title` | да | Название |
| `distanceKm` | да | Число км |
| `status` | нет | `planned` / `completed`; иначе можно вывести из даты |
| `officialUrl` | нет | Ссылка на организатора |
| `startTime` | нет | Напр. `09:30` |
| `clusterSchedule` | нет | Многострочный текст |
| `bibPickup` | нет | Выдача номеров |
| `bibNumber` | нет | Стартовый номер |
| `finishTime` | нет | Время H:MM:SS / HH:MM:SS; для PR — перевод в секунды |
| `notes` | нет | Заметки |
Опционально в миграциях: `created_at`, `updated_at`.
**Операции:** `GET` список/фильтры по году-месяцу при необходимости, `GET :id`, `POST`, `PATCH :id`, при необходимости `DELETE` — зафиксировать в `docs/backend.md`.
## 6. Поведение SPA
### 6.1 Форма «Добавить забег»
- Обязательно: дата, название, дистанция.
- Если дата **строго до сегодня** (локальная дата пользователя) — показать **результат** и **стартовый номер** (необязательны при первом сохранении).
- Если дата в будущем — поля результата скрыты.
- Опционально: чекбокс «Уже прошёл» для случая **сегодняшней** даты, чтобы открыть поля результата.
- Сохранение: `**POST`** на API → запись в БД.
### 6.2 Страница забега
- Все поля модели; для прошедших — акцент на результат и номер.
- Редактирование результата и номера **в любой момент**: `**PATCH`** на API, затем обновление состояния на клиенте (refetch / инвалидация кэша).
### 6.3 Личные рекорды
- Дистанции: 1, 5, 10, 15, 21.1, 42.2 км.
- Учитывать забеги с заполненным `finishTime` и дистанцией в пределах допуска к целевой (напр. 21.0975 → 21.1, 42.195 → 42.2).
- «Чужие» дистанции (2 км, 6 км, 30 км…) в таблицу PR по умолчанию **не** включаются.
- Расчёт на клиенте по данным, полученным с API.
### 6.4 Экраны
1. Главная — PR, навигация «Месяц» / «Год», ближайшие старты.
2. Месяц — селектор, список.
3. Год — сетка, маркеры, клик по дате → модалка со списком → карточка.
4. Карточка забега, форма добавления.
5. Доступность модалки: фокус, Esc, контраст.
## 7. Документация
Создать каталог `**docs/`**:
| Файл | Назначение |
| ------------------ | ------------------------------------------------------- |
| `docs/backend.md` | Docker, `DB`_*, миграции, seed, REST API, CORS |
| `docs/frontend.md` | Структура `frontend/`, `VITE`_*, сборка, контракт с API |
| `docs/ux-spa.md` | Сценарии, экраны, BEM, кратко про a11y |
Корневой `**README.md`**: описание проекта, быстрый старт, **ссылки на три файла выше**.
## 8. Риски и ограничения
- `**DB`_* и пароли** — только в окружении сервера API; в git — только `.env.example` без реальных секретов.
- Актуальные данные после работы в UI — в **БД**; файлы в git не синхронизируются с правками автоматически. Надёжный бэкап — **резервное копирование PostgreSQL**.
- Деплой: Postgres + процесс API с env + статическая раздача фронта (или иная схема хостинга — описать в README).
- Данные организаторов (расписание стартов) — **вручную** + ссылка; без парсинга чужих сайтов в объёме первой версии.
## 9. Ключевые пути после реализации
- `docker-compose.yml`, `.env.example`
- `backend/` — сервер, миграции, роуты
- `scripts/` или `backend/scripts/`**разовый** seed из CSV/JSON
- `import/races_2026_calendar.csv` — только вход seed (не рантайм)
- `public/data/races.json` — опциональный вход seed
- `frontend/src/api/` — клиент API
- `frontend/src/pages/`, `frontend/src/components/` — UI (BEM)
- `frontend/src/lib/distances.ts` — PR
- `docs/*.md`, `README.md`
## 10. Чеклист задач (implementation todos)
1. Монорепо: `frontend/` + `backend/`, BEM, токены, роутер.
2. Postgres в docker-compose, миграции таблицы `races`, бэкенд читает `DB`_*.
3. REST CRUD + разовый seed (CSV и/или JSON) → БД.
4. Клиент API на фронте, типы, загрузка данных для экранов и PR.
5. Экраны месяц и год, модалка по дате.
6. Форма добавления с условными полями; карточка с PATCH результата/номера.
7. Поля организатора на карточке.
8. A11y модалки, мобильная вёрстка, смоук-сценарии.
9. `docs/` + README + `.env.example`.
## 11. Идеи на будущее (тематика бега)
- Недельный объём / простой план подготовки.
- Импорт/экспорт JSON для бэкапа через API.
- Фильтры (город, дистанция), погода по дню старта.
- График динамики PR.
- Загрузка GPX / тренировки.
- Браузерные напоминания за N дней до старта.
- Сравнение результатов одной дистанции по годам.
- PWA для офлайн-просмотра (read-only кэш не заменяет БД без продуманной синхронизации).
---
*Документ создан как единый актуальный план в корне репозитория. При расхождениях с черновиками в IDE приоритет у этого файла.*
- Документация согласована с кодом: [`README.md`](README.md), [`docs/backend.md`](docs/backend.md), [`docs/backend-api-for-frontend.md`](docs/backend-api-for-frontend.md).
- Миграции и seed воспроизводимы; контракт API покрыт smoke-тестами в CI при необходимости с mock-БД.