Compare commits

...

2 Commits

Author SHA1 Message Date
Vaka.pro
007d899721 chore: drop agent/plan docs, unify .env for Docker stack
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
- Remove PLAN/agent instruction files; single root .env.example for DB + API
- Stack compose uses env_file .env; delete stack env example duplicate
- Refresh README, backend docs, API doc; trim gitignore/dockerignore

Made-with: Cursor
2026-04-07 00:30:29 +03:00
2cf01186e9 Merge pull request 'chore(docker): stack compose with env_file, no secrets in repo' (#6) from feature/docker-stack into main
Some checks failed
CI / build-and-test (push) Has been cancelled
Reviewed-on: #6
2026-04-06 20:26:37 +00:00
13 changed files with 100 additions and 672 deletions

View File

@@ -5,7 +5,6 @@
.env .env
.env.* .env.*
!.env.example !.env.example
docker-compose.stack.env
**/*.md **/*.md
.cursor .cursor
*.log *.log

View File

@@ -1,23 +1,33 @@
# ─── PostgreSQL ─────────────────────────────────────────────── # =============================================================================
# Не обязательны, если CALENDAR_RUN_MOCK_DB=1 (только HTTP API без БД). # Корневой шаблон окружения: локальная разработка, docker-compose.yml (Postgres)
# и docker-compose.stack.yml (backend + frontend в общей сети с Postgres).
# Скопируйте в .env и подставьте значения. Файл .env не коммитьте.
# =============================================================================
# ─── Подключение бэкенда к PostgreSQL ──────────────────────────
# Локально + docker-compose из этого репо: DB_HOST=localhost, DB_PORT=5432
# Backend в Docker рядом с Postgres: DB_HOST = имя контейнера/сервиса в той же
# docker-сети (например postgres_budget), DB_PORT=5432 (внутренний порт Postgres).
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_NAME=calendar_run DB_NAME=calendar_run
DB_USER=calendar_user DB_USER=calendar_user
DB_PASSWORD=calendar_pass DB_PASSWORD=replace_with_strong_secret
# ─── Backend API ────────────────────────────────────────────── # ─── Backend API ──────────────────────────────────────────────
# Порт: сначала читается PORT (если задан), иначе API_PORT, иначе 3001. # Порт процесса: сначала PORT, иначе API_PORT, иначе 3001.
# PORT=3001 # В docker-compose.stack.yml для контейнера задаётся PORT=3000 (см. compose).
# PORT=3000
API_PORT=3001 API_PORT=3001
# ─── Dev/CI: без PostgreSQL для smoke API (не для migrate/seed) ─ # ─── Режим без БД (только тесты / smoke API, не для migrate/seed) ─
# CALENDAR_RUN_MOCK_DB=1 # CALENDAR_RUN_MOCK_DB=1
# ─── CORS ──────────────────────────────────────────────────── # ─── CORS ────────────────────────────────────────────────────
# Allowed origin for the frontend (Vite dev server default) # Локальный Vite: http://localhost:5173
# Стек с фронтом на 3033: http://localhost:3033
CORS_ORIGIN=http://localhost:5173 CORS_ORIGIN=http://localhost:5173
# ─── Frontend (Vite) ───────────────────────────────────────── # ─── Frontend (Vite, локально из каталога frontend/) ─────────
# Public URL of the API, used in SPA code via import.meta.env # В Docker-образе фронта базовый URL API задаётся при сборке (/api), не из .env.
VITE_API_BASE_URL=http://localhost:3001 VITE_API_BASE_URL=http://localhost:3001

3
.gitignore vendored
View File

@@ -1,7 +1,4 @@
node_modules/ node_modules/
dist/ dist/
.env .env
docker-compose.stack.env
*.log *.log
*plan*
*PLAN*

37
PLAN.md
View File

@@ -10,25 +10,27 @@
## Модель данных `Race` (API — camelCase) ## Модель данных `Race` (API — camelCase)
| Поле | Тип | Описание | | Поле | Тип | Описание |
|------|-----|----------| | ----------------- | --------------------------------------------- | ----------------------------------------------- |
| `id` | string | Стабильный ключ, например `{YYYY-MM-DD}-{slug}` | | `id` | string | Стабильный ключ, например `{YYYY-MM-DD}-{slug}` |
| `date` | string | `YYYY-MM-DD` | | `date` | string | `YYYY-MM-DD` |
| `title` | string | Название | | `title` | string | Название |
| `distanceKm` | number | Дистанция, км | | `distanceKm` | number | Дистанция, км |
| `status` | `planned` \| `registered` \| `completed` \| null | Жизненный цикл старта | | `status` | `planned` | `registered` | `completed` | null | Жизненный цикл старта |
| `officialUrl` | string \| null | Сайт организатора | | `officialUrl` | string | null | Сайт организатора |
| `startTime` | string \| null | Время старта (строка, напр. `09:30`) | | `startTime` | string | null | Время старта (строка, напр. `09:30`) |
| `clusterSchedule` | string \| null | Расписание кластеров | | `clusterSchedule` | string | null | Расписание кластеров |
| `bibPickup` | string \| null | Выдача номеров | | `bibPickup` | string | null | Выдача номеров |
| `bibNumber` | string \| null | Стартовый номер | | `bibNumber` | string | null | Стартовый номер |
| `finishTime` | string \| null | Результат `H:MM:SS` или `MM:SS` | | `finishTime` | string | null | Результат `H:MM:SS` или `MM:SS` |
| `finishPlace` | string \| null | Место на финише (текст: «3», «3/120» и т.п.) | | `finishPlace` | string | null | Место на финише (текст: «3», «3/120» и т.п.) |
| `notes` | string \| null | Заметки | | `notes` | string | null | Заметки |
| `createdAt` | string | ISO, read-only | | `createdAt` | string | ISO, read-only |
| `updatedAt` | string \| null | ISO, read-only | | `updatedAt` | string | null | ISO, read-only |
PostgreSQL: `snake_case` столбцы, маппинг в [`backend/src/mappers/race.ts`](backend/src/mappers/race.ts).
PostgreSQL: `snake_case` столбцы, маппинг в `[backend/src/mappers/race.ts](backend/src/mappers/race.ts)`.
## HTTP API (минимум) ## HTTP API (минимум)
@@ -37,25 +39,26 @@ PostgreSQL: `snake_case` столбцы, маппинг в [`backend/src/mappers
- `GET /races` — список; query: `year`, `month` (целые; `month` 112). - `GET /races` — список; query: `year`, `month` (целые; `month` 112).
- `GET /races/:id`, `POST /races`, `PATCH /races/:id`, `DELETE /races/:id`. - `GET /races/:id`, `POST /races`, `PATCH /races/:id`, `DELETE /races/:id`.
Ошибки: JSON, единый стиль (`validation_error`, `not_found`, `conflict`, `database_unavailable`). Подробности — [`docs/backend-api-for-frontend.md`](docs/backend-api-for-frontend.md). Ошибки: JSON, единый стиль (`validation_error`, `not_found`, `conflict`, `database_unavailable`). Подробности — `[docs/backend-api-for-frontend.md](docs/backend-api-for-frontend.md)`.
## Seed ## Seed
- Файл [`import/races_2026_calendar.csv`](import/races_2026_calendar.csv). - Файл `[import/races_2026_calendar.csv](import/races_2026_calendar.csv)`.
- Стабильный `id`, upsert по `id`. Повторный запуск безопасен. - Стабильный `id`, upsert по `id`. Повторный запуск безопасен.
## Режим без PostgreSQL (dev/CI) ## Режим без PostgreSQL (dev/CI)
Переменная `CALENDAR_RUN_MOCK_DB=1` (или `true`): HTTP-обработчики используют заглушку пула **без** реальной БД. **Не использовать** для `npm run db:migrate` и `npm run seed` — нужен настоящий Postgres и `DB_*`. Переменная `CALENDAR_RUN_MOCK_DB=1` (или `true`): HTTP-обработчики используют заглушку пула **без** реальной БД. **Не использовать** для `npm run db:migrate` и `npm run seed` — нужен настоящий Postgres и `DB_`*.
## Frontend (SPA) ## Frontend (SPA)
- Маршруты: дашборд (`/`), список стартов (`/races`), карточка (`/races/:id`). - Маршруты: дашборд (`/`), список стартов (`/races`), карточка (`/races/:id`).
- Дашборд: ближайший старт, последний результат, PR, сезон, PR по ключевым дистанциям, сравнение завершённых стартов, при необходимости — лёгкая визуализация прогресса. - Дашборд: ближайший старт, последний результат, PR, сезон, PR по ключевым дистанциям, сравнение завершённых стартов, при необходимости — лёгкая визуализация прогресса.
- Список: будущие / прошедшие; фильтрация по году и месяцу через API. - Список: будущие / прошедшие; фильтрация по году и месяцу через API.
- Стили: BEM и дизайн-токены; ориентир по духу — [`agent-frontend-ui-instructions.md`](agent-frontend-ui-instructions.md). - Стили: BEM и дизайн-токены; ориентир по духу — `[agent-frontend-ui-instructions.md](agent-frontend-ui-instructions.md)`.
## Критерии готовности текущей итерации ## Критерии готовности текущей итерации
- Документация согласована с кодом: [`README.md`](README.md), [`docs/backend.md`](docs/backend.md), [`docs/backend-api-for-frontend.md`](docs/backend-api-for-frontend.md). - Документация согласована с кодом: `[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-БД. - Миграции и seed воспроизводимы; контракт API покрыт smoke-тестами в CI при необходимости с mock-БД.

View File

@@ -1,56 +1,47 @@
# Calendar Run # Calendar Run
Calendar Run is a races calendar project: a **backend API** (Express + PostgreSQL) and a **React SPA** for viewing and analyzing your race schedule. Монорепозиторий: **backend** (Express + PostgreSQL) и **frontend** (React + Vite) — календарь забегов.
Product scope and data model: [PLAN.md](PLAN.md). ## Переменные окружения
## Backend — quick start Один шаблон для локальной разработки и для Docker-стека: **[`.env.example`](.env.example)** → скопируйте в **`.env`** в корне репозитория.
1. Install dependencies: Там же перечислены **`DB_HOST`**, **`DB_PORT`**, **`DB_NAME`**, **`DB_USER`**, **`DB_PASSWORD`** (подключение бэкенда к БД), **`PORT`** / **`API_PORT`**, опционально **`CALENDAR_RUN_MOCK_DB`**, **`CORS_ORIGIN`**, а для локального Vite — **`VITE_API_BASE_URL`**.
- `cd backend`
- `npm install`
2. Configure environment variables. Copy the root template and edit:
```bash ## Backend — локально
cp .env.example .env
```
Use the **repository root** `.env` (the backend loads it via `backend/src/config.ts`). 1. `cd backend && npm install`
2. Корень: `cp .env.example .env`, задайте `DB_*` (и при необходимости `CORS_ORIGIN`).
3. Postgres: из корня `docker compose up -d` (см. [`docker-compose.yml`](docker-compose.yml)) — в compose используются те же `DB_NAME`, `DB_USER`, `DB_PASSWORD` из `.env`.
4. `cd backend && npm run db:migrate && npm run seed`
5. `npm run build && npm run dev`
- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` — required unless mock mode is on. Без PostgreSQL (только smoke API): в `.env` задайте `CALENDAR_RUN_MOCK_DB=1`; **`db:migrate` и `seed` с mock не использовать**.
- API port: `PORT` (takes precedence) or `API_PORT` (default `3001`).
- **No PostgreSQL (CI / local smoke):** set `CALENDAR_RUN_MOCK_DB=1` (or `true`). Real `DB_*` values are not required; the API uses in-memory stubs for SQL used by the HTTP routes. **Do not** use mock mode for `npm run db:migrate` or `npm run seed`.
3. With a real database: from repo root, `docker-compose up -d`, then `cd backend && npm run db:migrate && npm run seed`.
4. Build and run API:
```bash ## Frontend — локально
npm run build
npm run dev
```
## Frontend — quick start ```bash
cd frontend
cp .env.example .env
npm install
npm run dev
```
1. Configure the API URL for Vite (file is read from `frontend/`): Значение `VITE_API_BASE_URL` см. в **корневом** [`.env.example`](.env.example); для dev по умолчанию `http://localhost:3001`. У бэкенда `CORS_ORIGIN` должен совпадать с origin приложения (например `http://localhost:5173`).
```bash ## Docker: backend + frontend рядом с Postgres
cd frontend
cp .env.example .env
```
Edit `VITE_API_BASE_URL` if the API is not on `http://localhost:3001`. Используйте [`docker-compose.stack.yml`](docker-compose.stack.yml): общая **внешняя** сеть с контейнером Postgres (как в вашей инфраструктуре). В корне должен быть **`.env`** (из `.env.example`): `DB_HOST` — имя сервиса/контейнера Postgres в этой сети, `DB_PORT=5432`, плюс остальные `DB_*` и **`CORS_ORIGIN=http://localhost:3033`**, если заходите на фронт с хоста на порту 3033.
2. Install and run: ```bash
docker compose -f docker-compose.stack.yml up -d --build
docker compose -f docker-compose.stack.yml exec backend node dist/migrate.js
docker compose -f docker-compose.stack.yml exec backend node dist/seed.js
```
```bash Фронт в браузере обращается к API по префиксу **`/api`** (nginx в образе фронта проксирует на backend).
npm install
npm run dev
```
Default app URL: `http://localhost:5173`. The backend `CORS_ORIGIN` must match this origin (see root `.env.example`). ## Документация API и бэкенда
## Docs - [Шпаргалка API для фронта](docs/backend-api-for-frontend.md)
- [Эксплуатация backend](docs/backend.md)
- [Backend API for Frontend](docs/backend-api-for-frontend.md)
- [Backend operations](docs/backend.md)
- [Backend agent instruction](docs/agent-backend-instruction.md)
- [Backend sync fix instruction](docs/agent-fix-backend-sync-instruction.md)

View File

@@ -1,336 +0,0 @@
Ниже — инструкция для дизайн-агента, которая задаёт стиль, структуру и принципы интерфейса для твоего SPA. Она ориентирована на **минимализм, читаемость данных и удовольствие от использования**, а не на “перегруженный фитнес-дэшборд”.
# 🎯 Общая задача
Создать **чистый, современный, минималистичный интерфейс** для SPA-приложения бегуна, где пользователь:
- отслеживает свои старты (прошлые и будущие),
- видит прогресс и личные рекорды,
- сравнивает результаты,
- получает ощущение “контроля над своей беговой историей”.
---
# 🧭 Общая философия дизайна
### 1. “Спокойная сила”, а не “спортивная агрессия”
Не:
- кислотные цвета,
- перегруженные графики,
- “фитнес-клуб стиль 2015”.
Да:
- чистый интерфейс,
- воздух,
- акцент на данных,
- ощущение премиальности.
---
### 2. Минимум визуального шума
Каждый экран должен отвечать на вопрос:
> “Что здесь главное?”
Если не ясно — убрать лишнее.
---
### 3. Данные — главный герой
Интерфейс не про “украшения”, а про:
- результаты,
- прогресс,
- сравнение,
- числа.
---
# 🎨 Визуальный стиль
## Цвета
### Основа:
- фон: светлый (#F7F8FA или близко)
- карточки: белые
- текст:
- основной: почти чёрный (#111)
- вторичный: серый (#666)
### Акцент:
- 1 основной цвет:
- глубокий синий или изумрудный (например #2F6BFF или #1FA37A)
### Дополнительно:
- PR / успех: мягкий зелёный
- предупреждения: нейтральный жёлтый
- ошибки: мягкий красный
❗ Никаких “радуг”.
---
## Типографика
- современный sans-serif (Inter / SF Pro / аналог)
- крупные числа:
- результаты (время) — большие, жирные
- иерархия:
- H1 — экран
- H2 — блок
- body — данные
- caption — мета
---
## Отступы и сетка
- много воздуха
- карточки с padding 1624px
- равномерная вертикальная ритмика
- закругления: 1216px
---
# 🧱 Основные экраны
## 1. Главный экран (Dashboard)
### Цель:
дать быстрый ответ:
- что впереди
- где я сейчас
- какой прогресс
### Блоки:
#### 🔹 Ближайший старт
- название
- дата
- дистанция
- countdown (“через 12 дней”)
#### 🔹 Последний результат
- время
- дистанция
- место
- дата
#### 🔹 Личный рекорд
- по ключевой дистанции
- выделен визуально
#### 🔹 Сезон
- количество стартов
- лучший результат
- краткая статистика
---
## 2. Календарь стартов
### Два раздела:
- будущие
- прошедшие
### Карточка старта:
- название
- дата
- дистанция
- статус:
- планирую
- зарегистрирован
- пробежал
Минимум кликов.
---
## 3. Карточка старта
- название
- дата
- дистанция
- время
- темп
- место (если есть)
- заметки (опционально)
---
## 4. Личные рекорды (PR)
- список дистанций:
- 5K
- 10K
- 21.1
- 42.2
Для каждой:
- лучшее время
- дата
- старт
---
## 5. Сравнение стартов (ВАЖНО)
Это ключевая фича.
### UI:
таблица или карточки:
| Год | Время | Темп | Место |
| --- | ----- | ---- | ----- |
Можно добавить:
- стрелки (лучше/хуже)
- визуальный прогресс
---
# 📊 Графики
Минимально и аккуратно:
- линия прогресса по дистанции
- без перегруза
- без 10 линий сразу
---
# 🧩 Компоненты
- карточки
- таблицы (очень аккуратно)
- фильтры
- переключатели (tabs)
- кнопки (primary / secondary)
---
# 🧠 UX принципы
### 1. Минимум кликов
Любая частая задача:
→ максимум 23 клика
---
### 2. Быстрое считывание
Пользователь должен за 2 секунды понять:
- что это
- где он
- что делать
---
### 3. Консистентность
- одинаковые карточки
- одинаковые статусы
- одинаковые действия
---
# 🧪 Для аналитики (важно)
Заложить:
- зоны кликов
- понятные CTA
- отсутствие “пустых зон”
---
# 🖼️ Использование изображений
## Когда использовать:
- пустые состояния
- onboarding
- вдохновение
## Когда НЕ использовать:
- в данных
- в списках стартов
- в результатах
---
## Если генерировать изображения
Стиль:
- минимализм
- спорт без пафоса
- одиночный бегун
- город / парк
- мягкий свет
- утро / вечер
### Пример prompt:
> “minimalist photo of a runner jogging alone in a city park at sunrise, soft light, calm mood, no crowd, modern aesthetic”
---
# 🚫 Чего избегать
- перегруженных дашбордов
- 100 метрик сразу
- ярких кислотных цветов
- сложных графиков
- таблиц как в Excel
- лишних иконок
- “геймификации ради геймификации”
---
# 💡 Вдохновение (по духу)
Если бы нужно было описать стиль:
- как Notion, но для бегунов
- как Apple Fitness, но менее ярко
- как Strava, но более спокойно и чисто
---
# 🧭 Финальный ориентир
Интерфейс должен вызывать ощущение:
> “Я контролирую свою беговую историю и прогресс”
а не:
> “Я заполняю таблицу результатов”
---

View File

@@ -1,11 +0,0 @@
# Скопируйте в docker-compose.stack.env и подставьте свои значения.
# Файл docker-compose.stack.env не должен попадать в git (см. .gitignore).
DB_HOST=postgres
DB_PORT=5432
DB_NAME=calendar_run
DB_USER=calendar_user
DB_PASSWORD=replace_with_strong_secret
# Origin браузера для CORS (если заходите на фронт не с localhost — поменяйте)
CORS_ORIGIN=http://localhost:3033

View File

@@ -1,15 +1,19 @@
# Запуск приложения в сети с уже поднятым PostgreSQL (как у family-budget). # Backend + frontend в сети с уже поднятым PostgreSQL (external network).
# #
# 1) Скопируйте пример переменных (не коммитьте файл с паролями): # Подготовка: из корня репозитория скопируйте .env.example → .env и задайте DB_*
# cp docker-compose.stack.env.example docker-compose.stack.env # и CORS_ORIGIN (для фронта на :3033 — http://localhost:3033).
# отредактируйте docker-compose.stack.env
# #
# NPM / reverse proxy: проброс на порт фронта 3033 (внутри контейнера nginx слушает 80). # Сеть (имя должно существовать, как у вашего Postgres):
# Фронт в браузере ходит на /api → nginx проксирует на backend:3000. # docker network ls
# #
# Первая инициализация БД (один раз, когда Postgres уже доступен по DB_HOST): # Запуск:
# docker compose -f docker-compose.stack.yml up -d --build
#
# Миграции и seed (один раз после появления БД):
# docker compose -f docker-compose.stack.yml exec backend node dist/migrate.js # docker compose -f docker-compose.stack.yml exec backend node dist/migrate.js
# docker compose -f docker-compose.stack.yml exec backend node dist/seed.js # docker compose -f docker-compose.stack.yml exec backend node dist/seed.js
#
# NPM: проброс на порт 3033. Браузер ходит на /api → nginx во фронте → backend:3000.
services: services:
backend: backend:
@@ -18,7 +22,7 @@ services:
dockerfile: Dockerfile.backend dockerfile: Dockerfile.backend
container_name: runners-calendar-backend container_name: runners-calendar-backend
env_file: env_file:
- docker-compose.stack.env - .env
environment: environment:
- PORT=3000 - PORT=3000
ports: ports:

View File

@@ -1,162 +0,0 @@
# Инструкция агенту: реализация бэкенда по [PLAN.md](../PLAN.md)
Документ для ИИ-агента или разработчика, который создаёт **backend** монорепозитория «календарь забегов». Продуктовые цели и модель данных — в корневом **PLAN.md**; здесь — порядок работ, ограничения и **обязательные итоговые артефакты для фронтенда**.
---
## 0. Ограничение: нет возможности проверить подключение к БД
1. **Всё равно реализовать полноценный бэкенд «как для прод»**: миграции SQL, пул подключений `pg`, переменные `DB_*`, `docker-compose.yml` с PostgreSQL в корне репозитория.
2. **Не блокировать работу отсутствием живой БД у исполнителя:**
- код миграций и seed должен быть **валидным и согласованным** с PLAN;
- при старте API при невозможности подключиться к БД — **ясное сообщение в лог** и **корректный HTTP-ответ** на зависящих от БД маршрутах (например 503 с телом `{"error":"database_unavailable",...}`) **или** падение процесса на старте с понятной ошибкой (выбрать одну стратегию и описать её в `docs/backend.md`).
3. **Режим без Postgres для dev/CI** согласован с [PLAN.md](../PLAN.md) и `docs/backend.md`: переменная `CALENDAR_RUN_MOCK_DB=1` (или `true`) включает in-memory заглушку пула **только** для HTTP-слоя. Для **`npm run db:migrate`** и **`npm run seed`** нужен реальный PostgreSQL и `DB_*`; mock для миграций/seed не используется.
4. Автотесты, требующие Docker/Postgres, помечать как **опциональные** или давать инструкцию «как прогнать локально», не считая провал из-за отсутствия БД у агента блокером для merge кода.
---
## 1. Ветка и расположение в репо
- Если репозиторий под git: создать ветку `feature/backend-api` (или аналог по соглашению команды).
- Каталог **`backend/`** в корне проекта рядом с будущим `frontend/`.
- Корневой **`docker-compose.yml`** — сервис `postgres` (порт, пользователь, БД, пароль — согласовать с `.env.example`).
---
## 2. Порядок реализации (обязательный)
### Шаг A. Каркас
- Node **LTS**, **TypeScript**, **`backend/package.json`**.
- Фреймворк: **Fastify** или **Express** (выбрать один, не смешивать).
- Загрузка env: `dotenv` или встроенные средства; **валидация** наличия `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` при старте (или при первом запросе к БД — но тогда задокументировать).
- Сервер слушает порт из **`PORT`** или **`API_PORT`** (значение по умолчанию, напр. `3001`, указать в `.env.example`).
### Шаг B. CORS
- Читать **`CORS_ORIGIN`** (например `http://localhost:5173` для Vite в dev). В prod — origin фронта.
- Разрешить методы и заголовки, нужные для `GET/POST/PATCH` + `Content-Type: application/json`.
### Шаг C. Миграции
- Каталог миграций, напр. `backend/migrations/` с нумерованными SQL-файлами **или** один миграционный runner (node-pg-migrate, graphile-migrate, собственный скрипт — на выбор, зафиксировать в `docs/backend.md`).
- Первая миграция: таблица **`races`** со столбцами, соответствующими PLAN (см. раздел 3 ниже).
- Команда **`npm run db:migrate`** (или эквивалент в `backend/`) — идемпотентное накатывание на чистую БД.
### Шаг D. Доступ к данным
- Клиент **`pg`**: `Pool` с параметрами из `DB_*`.
- Слой репозитория или прямые запросы в обработчиках — на усмотрение, без лишних абстракций сверх задачи.
### Шаг E. HTTP API
Реализовать минимум:
| Метод | Путь | Назначение |
|--------|------|------------|
| `GET` | `/health` | Liveness: процесс жив; **не обязан** проверять БД (или опционально — задокументировать). |
| `GET` | `/ready` (опционально) | Readiness: проверка соединения с БД — полезно для оркестраторов. |
| `GET` | `/races` | Список забегов; **query**: `year`, `month` (112) — фильтрация для экранов календаря; без параметров — все строки или разумный лимит + документация пагинации если добавите позже. |
| `GET` | `/races/:id` | Одна запись по `id`. |
| `POST` | `/races` | Создание; тело JSON в **camelCase** как в PLAN. |
| `PATCH` | `/races/:id` | Частичное обновление; только переданные поля. |
| `DELETE` | `/races/:id` | Опционально по PLAN; если не нужен фронту — можно не делать, но тогда явно написать в документации «удаление не поддерживается». |
**Ошибки:**
- 400 — валидация тела/параметров.
- 404 — нет `id`.
- 409 — конфликт уникального `id` при POST (если клиент прислал уже существующий).
- 503 или 500 — недоступна БД (согласовать с разделом 0).
Формат ошибки: JSON, единообразно, напр. `{"error":"validation_error","details":[...]}`.
### Шаг F. Seed (разовый скрипт)
- Команда **`npm run seed`** в `backend/` (или корне монорепо с `workspace` — единообразно).
- Читает **`import/races_2026_calendar.csv`** (путь от корня репо); парсинг с учётом кавычек в поле названия.
- Генерирует **`id`** стабильно: например `{date}-{slug-from-title}`; при коллизии — суффикс или upsert по `id`.
- **`INSERT ... ON CONFLICT (id) DO UPDATE`** (upsert) — удобно для повторного запуска seed.
- Seed **не** вызывается из HTTP handlers.
### Шаг G. Корневой `.env.example`
- Все переменные: `DB_*`, `PORT`/`API_PORT`, `CORS_ORIGIN`.
- **Без** реальных паролей; комментарии к каждой переменной.
---
## 3. Соответствие полей PLAN ↔ SQL ↔ JSON API
**В JSON API (запрос/ответ)****camelCase**, как в PLAN:
`id`, `date`, `title`, `distanceKm`, `status` (`planned` \| `registered` \| `completed`), `officialUrl`, `startTime`, `clusterSchedule`, `bibPickup`, `bibNumber`, `finishTime`, `finishPlace`, `notes`.
**В PostgreSQL****snake_case**, например:
| SQL column | Тип (рекомендация) |
|------------|---------------------|
| `id` | `TEXT` PRIMARY KEY |
| `race_date` | `DATE` |
| `title` | `TEXT` |
| `distance_km` | `NUMERIC(6,3)` |
| `status` | `TEXT` CHECK (опционально) |
| `official_url` | `TEXT` |
| `start_time` | `TEXT` |
| `cluster_schedule` | `TEXT` |
| `bib_pickup` | `TEXT` |
| `bib_number` | `TEXT` |
| `finish_time` | `TEXT` |
| `finish_place` | `TEXT` |
| `notes` | `TEXT` |
| `created_at` | `TIMESTAMPTZ` DEFAULT now() |
| `updated_at` | `TIMESTAMPTZ` |
Маппинг **строго** в одном модуле (например `backend/src/mappers/race.ts`), чтобы фронт всегда видел camelCase.
**Типы `date`:** в API строка `YYYY-MM-DD`. **`distanceKm`:** число. **`finishTime`:** строка времени как в PLAN; бэкенд **не обязан** парсить для бизнес-логики (PR считает фронт), но может валидировать формат по желанию.
---
## 4. Обязательный итог для упрощения фронтенда
После того как код бэкенда готов, агент **обязан** добавить в репозиторий документ:
### Файл: `docs/backend-api-for-frontend.md`
В нём **кратко и без пробелов в фактах**:
1. **Base URL** — что подставлять во фронт (`VITE_API_BASE_URL`), пример для dev.
2. **CORS** — какой `CORS_ORIGIN` ожидается в dev.
3. **Таблица эндпоинтов** — метод, путь, query, тело запроса (пример JSON), пример успешного ответа, коды ошибок.
4. **Модель `Race`** — перечень полей в **camelCase** с типами и обязательностью для POST vs PATCH.
5. **Фильтр списка** — как именно работают `year` и `month` на `GET /races` (включая границы: пустой месяц, только год и т.д.).
6. **Идемпотентность seed** — одна фраза: upsert по `id`, откуда берётся CSV.
7. **Поведение при недоступной БД** — что возвращает API / что в логах (как в реализации).
Дополнительно можно дублировать суть в **`docs/backend.md`** (общая эксплуатация: docker, migrate, seed, запуск), но **`backend-api-for-frontend.md`** — главная «шпаргалка» для разработчика SPA.
---
## 5. Критерии готовности (чеклист агента)
- [ ] `docker-compose.yml` поднимает Postgres с параметрами, совместимыми с `.env.example`.
- [ ] Миграция создаёт `races`; есть команда миграции.
- [ ] Реализованы `GET /races`, `GET /races/:id`, `POST /races`, `PATCH /races/:id` согласно PLAN.
- [ ] Реализован seed из `import/races_2026_calendar.csv`.
- [ ] `GET /health` (и при наличии `/ready` — описано).
- [ ] Корневой `.env.example` обновлён.
- [ ] Написан **`docs/backend-api-for-frontend.md`** (раздел 4).
- [ ] `docs/backend.md` содержит команды: установка зависимостей, migrate, seed, `npm run dev` для API.
---
## 6. Не входит в объём бэкенда (напоминание)
- Авторизация, пользователи, сессии.
- Парсинг сайтов организаторов.
- Отдача статики фронта с того же процесса (фронт — отдельно Vite/build).
---
*Конец инструкции. Источник требований к продукту — всегда [PLAN.md](../PLAN.md).*

View File

@@ -1,77 +0,0 @@
# Инструкция агенту: устранение рассинхронизации backend с планом/контрактом
Документ описывает, как выполнить план исправлений **только через изменения кода** (без правок существующей документации в `docs/`).
## 1) Ветка и границы задачи
- Создать отдельную ветку по best practice, например: `fix/backend-api-validation-and-runtime-sync`.
- Не менять существующие файлы документации в `docs/` как способ "починить" замечания.
- Исправления вносятся в runtime-код и обязательные артефакты репозитория.
## 2) Обязательные изменения в коде
### A. Строгая валидация `GET /races`
Файл: `backend/src/routes/races.ts`
- Добавить явную проверку `year`:
- целое число;
- при невалидном значении вернуть `400`:
- `{"error":"validation_error","details":[...]}`
- Добавить явную проверку `month`:
- целое число в диапазоне `1..12`;
- при невалидном значении вернуть `400` в том же формате.
- Исключить передачу `NaN`/некорректных значений в SQL-параметры.
### B. Разделение ошибок валидации и ошибок БД
Файл: `backend/src/routes/races.ts`
- `400` использовать только для ошибок входа (query/body/params).
- `503 {"error":"database_unavailable"}` использовать только для технической недоступности БД.
- Сохранить единый JSON-формат ошибок во всех CRUD-маршрутах.
### C. Выравнивание конфигурации порта
Файл: `backend/src/config.ts`
- Поддержать оба env-подхода:
- приоритет `PORT`,
- затем `API_PORT`,
- затем fallback `3001`.
### D. Обязательный root-артефакт
Файл: `README.md` (в корне)
- Создать базовый `README.md` с:
- кратким описанием проекта,
- минимальным quick start,
- ссылками на текущие документы backend/API.
## 3) Допустимая реорганизация кода
- Можно добавить небольшие локальные helper-функции в `backend/src/routes/races.ts`.
- При необходимости можно вынести общие mapper/validator-хелперы в `backend/src/mappers/race.ts`, если это уменьшает дублирование.
- Не усложнять архитектуру: только то, что нужно для контракта и устойчивого поведения API.
## 4) Проверка результата
Минимум выполнить:
1. `npm run build` в `backend/` (типизация/сборка).
2. Проверка диагностики/линтов по измененным backend-файлам.
3. Smoke-сценарии API:
- `GET /health` -> `200`,
- `GET /ready` -> `200` при доступной БД или `503` при недоступной,
- `GET /races?year=bad` -> `400`,
- `GET /races?month=13` -> `400`,
- `GET /races?year=2026&month=5` -> корректный `200` и данные/пустой массив.
## 5) Критерии готовности (Definition of Done)
- Контракт валидации `GET /races` соблюден в runtime.
- Валидационные ошибки не маскируются под `database_unavailable`.
- Конфиг порта поддерживает `PORT` и `API_PORT` с правильным приоритетом.
- В репозитории есть корневой `README.md`.
- Никакие существующие документы в `docs/` не менялись для "закрытия" замечаний.

View File

@@ -8,6 +8,8 @@ VITE_API_BASE_URL=http://localhost:3001
В коде SPA: `import.meta.env.VITE_API_BASE_URL`. В коде SPA: `import.meta.env.VITE_API_BASE_URL`.
В Docker-стеке из репозитория образ фронта собирается с **`VITE_API_BASE_URL=/api`**: запросы идут на тот же origin, nginx проксирует `/api` на backend (см. `docker/nginx.frontend.conf`).
## 2. CORS ## 2. CORS
В dev-режиме бэкенд ожидает переменную: В dev-режиме бэкенд ожидает переменную:

View File

@@ -78,6 +78,8 @@ API слушает порт: **`PORT`** (если задан), иначе **`API
| `API_PORT` | Порт API-сервера | `3001` | | `API_PORT` | Порт API-сервера | `3001` |
| `CORS_ORIGIN` | Разрешённый origin для CORS | `http://localhost:5173` | | `CORS_ORIGIN` | Разрешённый origin для CORS | `http://localhost:5173` |
Для локального Vite в корневом `.env.example` также указан **`VITE_API_BASE_URL`** (читает только фронт из `frontend/`). В Docker-стеке базовый URL API задаётся при **сборке** образа фронта (`/api`), не через этот файл.
**Без mock:** при отсутствии любой из `DB_*` процесс падает при старте: `Missing required environment variable: <NAME>`. **Без mock:** при отсутствии любой из `DB_*` процесс падает при старте: `Missing required environment variable: <NAME>`.
**С `CALENDAR_RUN_MOCK_DB=1`:** переменные `DB_*` не обязательны; реальный пул не поднимается. **Не использовать** mock для `npm run db:migrate` и `npm run seed` — нужен настоящий Postgres и корректные `DB_*`. **С `CALENDAR_RUN_MOCK_DB=1`:** переменные `DB_*` не обязательны; реальный пул не поднимается. **Не использовать** mock для `npm run db:migrate` и `npm run seed` — нужен настоящий Postgres и корректные `DB_*`.
@@ -111,3 +113,9 @@ backend/
├── package.json ├── package.json
└── tsconfig.json └── tsconfig.json
``` ```
## Docker: стек backend + frontend
Файл [`docker-compose.stack.yml`](../docker-compose.stack.yml) поднимает API и nginx со статикой SPA в **внешней** сети Docker (рядом с уже запущенным Postgres). Переменные — в **корневом** `.env` (шаблон [`.env.example`](../.env.example)): как минимум `DB_*`, `CORS_ORIGIN` (для выдачи фронта на порту 3033 задайте `http://localhost:3033`). Перед первым `up` файл `.env` должен существовать.
Порядок после старта контейнеров: `node dist/migrate.js` и `node dist/seed.js` внутри контейнера `backend` (см. комментарии в compose-файле).

View File

@@ -1,2 +1,2 @@
# Base URL of the Calendar Run API (must match CORS_ORIGIN on the backend) # Для локального npm run dev. Полный список переменных — в корневом .env.example репозитория.
VITE_API_BASE_URL=http://localhost:3001 VITE_API_BASE_URL=http://localhost:3001