Перейти к содержанию

DZ Hub — устройство проекта

Полное описание проекта: из чего он состоит, как запускается, что делает каждый файл конфигурации и каждый слой кода. Состояние на 2026-10-03: фронтенд — ветка create-auth-flow-and-connect-with-auth-api (коммит e3cc09a), бэкенд — рабочая папка.

О продукте (что и зачем делаем) — MVP.md. Здесь — как это сделано.

Содержание

  1. Общая картина
  2. Быстрый старт
  3. Структура рабочей папки
  4. Как фронтенд и бэкенд работают вместе
  5. Фронтенд
  6. Бэкенд
  7. Git и ветки
  8. Известные недоделки и долги
  9. Шпаргалка

1. Общая картина

DZ Hub — инструмент для парашютного аэродрома: прыжковые дни, взлёты, запись на них, кабинет парашютиста и панель организатора. Сейчас реализованы регистрация, вход, выход и текущий пользователь. Остальное (дни, взлёты, допуски) есть только в мок-прототипе на ветке try/associations.

┌──────────────────────────┐   HTTP + JSON (camelCase)    ┌──────────────────────────┐
│  Фронтенд                │   Authorization: Bearer JWT  │  Бэкенд                  │
│  React 19 + Vite         │ ───────────────────────────► │  FastAPI + SQLAlchemy    │
│  http://localhost:5173   │   http://localhost:8000      │  /api/v1/...             │
└──────────────────────────┘          /api/v1             └────────────┬─────────────┘
                                                                       │ asyncpg
                                                          ┌────────────▼─────────────┐
                                                          │  PostgreSQL 18 (Docker)  │
                                                          │  localhost:5433          │
                                                          └──────────────────────────┘
Часть Стек Где Git
Фронтенд React 19, TypeScript 6, Vite 8, Tailwind 4, TanStack Query 5 frontend/ свой репозиторий, есть origin
Бэкенд Python 3.13, FastAPI, SQLAlchemy 2 (async), Alembic, uv backend/ пока не git-репозиторий
Документы Markdown docs/ не в git

Порты, которые нужно помнить:

Порт Кто Почему такой
5173 Vite dev-сервер (фронтенд) жёстко зафиксирован: под него настроен CORS бэкенда
8000 FastAPI (бэкенд) стандарт uvicorn
5433 PostgreSQL в Docker не 5432, чтобы не конфликтовать с локальным PostgreSQL

2. Быстрый старт

Нужны: Node.js ≥ 22, Docker (у вас — OrbStack), uv.

# 1. Бэкенд: база + API в Docker, миграции применяются при старте
cd backend
make install        # зависимости + .env из .env.example (если .env ещё нет)
make up             # http://localhost:8000, документация API: http://localhost:8000/api/docs

# 2. Фронтенд
cd ../frontend
npm install
npm run dev         # http://localhost:5173 → «Регистрация» → создать аккаунт

Без бэкенда: в frontend/.env.local положить VITE_ENABLE_MOCKS=true, вход demo@example.md / password123.


3. Структура рабочей папки

DZHub/                        ← рабочая папка (открыта в VS Code), сама не git
├── README.md                 ← краткое описание частей
├── .vscode/                  ← настройки VS Code для всей папки (см. 5.13)
├── docs/
│   ├── MVP.md                ← о чём договорились по продукту
│   └── PROJECT.md            ← этот файл
├── frontend/                 ← git-репозиторий фронтенда
└── backend/                  ← бэкенд (git ещё не инициализирован)

4. Как фронтенд и бэкенд работают вместе

4.1. Адрес бэкенда

Фронтенд узнаёт адрес бэкенда так:

VITE_APP_ENV=development  ──►  src/config/backend.ts           ──►  env.apiUrl
(из .env)                      backendUrls.development              http://localhost:8000/api/v1
                               = 'http://localhost:8000'
                               + API_PREFIX '/api/v1'

Новое окружение (staging, production) = новая строка в backendUrls. Переменная VITE_APP_ENV автоматически начнёт принимать новое значение (схема строится из ключей таблицы). Неизвестное значение → приложение не запустится и покажет ошибку конфигурации.

4.2. CORS

Браузер ходит на localhost:8000 напрямую, а страница открыта с localhost:5173 — это разные «источники» (origin), поэтому браузер требует разрешения от бэкенда (CORS).

  • Бэкенд: CORS_ORIGINS=["http://localhost:5173"] в backend/.env. Если список пуст, CORS-middleware вообще не подключается (app/main.py).
  • Фронтенд: в vite.config.ts стоит strictPort: true. Если 5173 занят, Vite не запустится, а не уйдёт на 5174, где CORS запросы заблокирует.
  • .env бэкенда читается при старте контейнера. Поменяли CORS_ORIGINS → docker compose up -d --force-recreate api (или make restart).

4.3. Контракт API

Соглашение Как устроено
Формат JSON camelCase. На бэкенде поля в snake_case, перевод делает CamelModel (alias_generator=to_camel)
Префикс /api/v1
Ошибки формат FastAPI: {"detail": "текст"} или {"detail": [{loc, msg, type}]} для 422
Авторизация Authorization: Bearer <JWT>
Типы на фронте пока вручную в frontend/src/shared/api/clients/*.ts, повторяют Pydantic-схемы. План — генерировать из OpenAPI (/api/v1/openapi.json)

4.4. Эндпоинты, которые существуют сейчас

Метод и путь Доступ Ответ Ошибки
GET /api/v1/health все {"status":"ok"} — процесс жив —
GET /api/v1/health/ready все {"status":"ok"} — база доступна 503
POST /api/v1/auth/register все 201 + TokenResponse 409 email занят, 422 валидация
POST /api/v1/auth/login все 200 + TokenResponse 401 неверные данные, 403 аккаунт отключён
GET /api/v1/auth/me с токеном UserPublic 401
POST /api/v1/auth/logout с токеном 204 401

Тела запросов и ответов:

// RegisterRequest
{ "email": "a@b.md", "password": "8–128 символов", "firstName": "1–100", "lastName": "1–100" }

// LoginRequest
{ "email": "a@b.md", "password": "..." }

// TokenResponse
{ "accessToken": "eyJ...", "tokenType": "bearer", "user": UserPublic }

// UserPublic
{ "id": "uuid v7", "email": "a@b.md", "firstName": "Ana", "lastName": "Rusu", "fullName": "Ana Rusu" }

4.5. Жизненный цикл сессии (сквозной сценарий)

Регистрация/вход
  RegisterForm ──► useRegister() ──► authClient.register() ──► POST /auth/register
                                                                   │ 201 {accessToken, user}
  tokenStorage.set(accessToken)  ◄─────────────────────────────────┘
  queryClient.setQueryData(['auth','session'], user)   ← пользователь «вошёл», без лишнего /me
  navigate('/')

Перезагрузка страницы
  useSession() → токен в localStorage есть? → GET /auth/me → user → «вошёл»
                                          нет → «аноним» → RequireAuth → /login

Токен протух / подделан (любой запрос с токеном вернул 401)
  http-client → unauthorizedHandler → tokenStorage.clear() + session = null → /login

Выход
  POST /auth/logout (ошибку игнорируем) → удалить токен → очистить весь кэш Query → /login

Важно: токены на сервере не хранятся. «Выход» = фронтенд удаляет свой токен. Сам токен остаётся валидным до истечения срока (7 дней). Отозвать его сейчас нельзя.


5. Фронтенд

5.1. Стек и зависимости

Рантайм (dependencies, попадают в бандл):

Пакет Версия Зачем
react, react-dom 19 UI
react-router 8 маршрутизация (data router: createBrowserRouter)
@tanstack/react-query 5 данные с сервера: кэш, загрузка, повторы, мутации
react-hook-form + @hookform/resolvers 7 / 5 формы
zod 4 схемы валидации: формы и конфиг окружения
i18next, react-i18next, i18next-browser-languagedetector 26 / 17 / 8 переводы RO/RU
@radix-ui/react-slot, @radix-ui/react-label — примитивы для компонентов в стиле shadcn/ui
class-variance-authority (cva) — варианты стилей компонентов (variant, size)
clsx + tailwind-merge — склейка классов без конфликтов (функция cn)
lucide-react 1 иконки

Разработка (devDependencies):

Пакет Зачем
vite 8, @vitejs/plugin-react сборщик и dev-сервер
typescript ~6.0, @types/* типы
tailwindcss 4 + @tailwindcss/vite стили (настраиваются в CSS, без tailwind.config.js)
vitest 5, jsdom, @testing-library/* тесты компонентов
msw 2 моки API для тестов и для режима без бэкенда
eslint 9 + плагины, typescript-eslint линтер
prettier 3 + prettier-plugin-tailwindcss форматирование и сортировка Tailwind-классов
husky + lint-staged проверки перед коммитом
@tanstack/react-query-devtools панель отладки запросов в dev

engines.node: ">=22" — на старом Node установка предупредит.

5.2. npm-скрипты

Команда Что делает
npm run dev dev-сервер Vite на http://localhost:5173 (режим development)
npm run build tsc -b (проверка типов) → vite build → папка dist/
npm run preview раздать собранный dist/ локально
npm run typecheck tsc -b: проверка типов всех проектов TS
npm run lint / lint:fix ESLint / с автоисправлением
npm run format / format:check Prettier: исправить / только проверить
npm test / test:watch Vitest: один прогон / в режиме наблюдения
npm run check typecheck + lint + format:check + test — как в CI
prepare запускается сам после npm install: ставит git-хуки Husky

5.3. Структура папок

frontend/
├── index.html                 ← HTML-оболочка, <title>%VITE_APP_NAME%</title>, lang="ro"
├── public/
│   ├── favicon.svg
│   └── mockServiceWorker.js   ← файл MSW для Service Worker (сейчас не используется, см. 8)
├── src/
│   ├── main.tsx               ← точка входа: конфиг → моки → i18n → React
│   ├── index.css              ← Tailwind + дизайн-токены
│   ├── vite-env.d.ts          ← типы переменных import.meta.env
│   ├── config/
│   │   ├── backend.ts         ← таблица «окружение → адрес бэкенда»
│   │   ├── env.ts             ← проверка и нормализация всех настроек (zod)
│   │   └── env.test.ts
│   ├── app/                   ← СБОРКА приложения
│   │   ├── App.tsx            ← AppProviders + RouterProvider
│   │   ├── router.tsx         ← все маршруты
│   │   ├── providers/AppProviders.tsx  ← QueryClient, devtools, SessionWatcher
│   │   ├── layouts/           ← PublicLayout (гости), AppLayout (вошедшие)
│   │   └── pages/             ← HomePage, NotFoundPage, RouteErrorPage
│   ├── features/              ← БИЗНЕС-МОДУЛИ
│   │   └── auth/
│   │       ├── index.ts       ← публичный API модуля
│   │       ├── api/session.ts ← хуки: useSession, useLogin, useRegister, useLogout…
│   │       ├── model/schemas.ts ← zod-схемы форм входа и регистрации
│   │       ├── components/    ← LoginForm, RegisterForm, guards (+ тесты рядом)
│   │       └── pages/         ← LoginPage, RegisterPage
│   ├── shared/                ← ОБЩЕЕ, без бизнес-логики
│   │   ├── api/
│   │   │   ├── http-client.ts ← fetch-обёртка: URL, токен, таймаут, ошибки
│   │   │   ├── api-error.ts   ← класс ApiError
│   │   │   ├── token-storage.ts ← где лежит токен (localStorage)
│   │   │   ├── query-client.ts  ← настройки TanStack Query
│   │   │   ├── clients/       ← КЛИЕНТЫ БЭКЕНДА, по файлу на модуль API
│   │   │   │   ├── auth.ts    ←   типы контракта + authClient
│   │   │   │   └── index.ts
│   │   │   └── index.ts
│   │   ├── config/routes.ts   ← все URL приложения
│   │   ├── lib/               ← cn(), getErrorMessage()
│   │   └── ui/                ← Button, Card, Input, Label, FormField, Spinner, LanguageSwitcher
│   ├── i18n/
│   │   ├── index.ts           ← инициализация i18next
│   │   ├── resources.ts       ← реестр namespace'ов
│   │   ├── i18next.d.ts       ← типизация ключей
│   │   └── locales/{ro,ru}/{common,auth}.json
│   ├── mocks/                 ← фейковый бэкенд (MSW-хендлеры)
│   │   ├── data.ts            ← in-memory «база»: демо-пользователь
│   │   ├── handlers/          ← auth.ts, index.ts
│   │   ├── browser.ts         ← моки в браузере (подмена fetch)
│   │   ├── server.ts          ← моки в тестах (msw/node)
│   │   └── utils.ts           ← apiPath()
│   └── test/
│       ├── setup.ts           ← глобальная настройка тестов
│       └── render.tsx         ← renderWithProviders()
├── Dockerfile, nginx/         ← production-образ (см. 5.12)
├── .github/workflows/ci.yml   ← CI
└── конфиги: package.json, tsconfig*.json, vite.config.ts, vitest.config.ts,
    eslint.config.js, .prettierrc.json, .editorconfig, .env*, .husky/

5.4. Слои и правила зависимостей

app ──────► features ──────► shared
  └────────────────────────────▲
       config и i18n доступны всем
Слой Что можно Что нельзя
app импортировать всё —
features/<x> shared, config, i18n, другие модули только через @/features/<y> лезть внутрь чужого модуля (@/features/y/components/...)
shared config, i18n импортировать app и features

Ещё правило: внутри модуля — относительные пути (../api/session), между слоями — алиас @/. Всё это проверяет ESLint (см. 5.11), нарушение = ошибка линтера.

Публичный API модуля — его index.ts. Сейчас @/features/auth отдаёт: useSession, useLogout, useUnauthorizedRedirect, тип Session, RequireAuth, RedirectIfAuthenticated, тип User, LoginPage, RegisterPage.

5.5. Запуск приложения (main.tsx)

Модули грузятся по очереди через динамический import(), и это сделано специально:

  1. import('@/config/env') — проверка конфига. Если он кривой, parseEnv бросает EnvValidationError, и bootstrap().catch пишет текст ошибки прямо в #root красным. Вместо белого экрана вы видите, какой параметр не так.
  2. Если VITE_ENABLE_MOCKS=true — startMocks() (подмена fetch). Условие import.meta.env.VITE_ENABLE_MOCKS === 'true' Vite вычисляет при сборке, поэтому при false код моков полностью вырезается из бандла.
  3. import('@/i18n') — переводы (синхронная инициализация, без «мигания» ключей).
  4. React, ReactDOM и App грузятся параллельно и монтируются в <StrictMode>.

5.6. Конфигурация: .env-файлы и env.ts

Порядок загрузки (Vite, каждый следующий перекрывает предыдущий): .env → .env.[mode] → .env.local → .env.[mode].local. Режим (mode): development для npm run dev, production для build, test для Vitest.

Файл В git Когда Содержимое сейчас
.env да всегда значения по умолчанию для всех параметров
.env.development да npm run dev моки выключены, devtools включены, порт 5173
.env.test да npm test VITE_ENABLE_MOCKS=false (в тестах моки включает setup.ts, а не эта переменная)
.env.example да никогда шаблон с описанием каждого параметра
.env.local нет (*.local в .gitignore) всегда ваши личные переопределения

Все параметры:

Переменная По умолчанию Значения Что делает
VITE_APP_NAME "DZ Hub" строка название в шапке и <title>
VITE_APP_ENV development ключи backendUrls выбирает адрес бэкенда
VITE_API_TIMEOUT_MS 15000 целое > 0 таймаут каждого HTTP-запроса
VITE_ENABLE_MOCKS false true / false отвечать на API-запросы моками вместо бэкенда
VITE_DEFAULT_LOCALE ro код языка язык, если язык браузера не поддерживается; обязан входить в список ниже
VITE_SUPPORTED_LOCALES ro,ru через запятую доступные языки
VITE_ENABLE_QUERY_DEVTOOLS false (true в dev) true / false панель TanStack Query; работает только в dev-сборке
DEV_SERVER_PORT 5173 число порт dev-сервера (без VITE_ → в браузер не попадает)

⚠️ Всё с префиксом VITE_ вшивается в JavaScript-бандл и видно любому. Никаких секретов в таких переменных.

src/config/env.ts — единственное место, где читается import.meta.env: - zod-схема проверяет типы, превращает 'true' в true, '15000' в число, 'ro, ru' в ['ro','ru'], проверяет, что язык по умолчанию входит в список; - наружу отдаёт объект env с удобными полями: appName, appEnv, backendUrl, apiUrl, apiTimeoutMs, enableMocks, defaultLocale, supportedLocales, enableQueryDevtools (= DEV && VITE_ENABLE_QUERY_DEVTOOLS), isDev.

Добавить параметр: схема в env.ts + тип в vite-env.d.ts + описание в .env.example (+ значение в .env).

5.7. HTTP-слой (shared/api)

http-client.ts — обёртка над fetch. Каждый запрос: 1. URL = env.apiUrl + путь (+ query-параметры, null/undefined пропускаются); 2. заголовки: Accept: application/json; Content-Type: application/json, если есть тело; Authorization: Bearer <токен>, если токен есть и не передан auth: false; 3. таймаут через AbortSignal.timeout(env.apiTimeoutMs), объединённый с сигналом отмены от TanStack Query (AbortSignal.any); 4. ошибки превращаются в ApiError:

Ситуация kind status
сервер ответил 4xx/5xx http код ответа; detail → message, массив 422 → issues
нет сети / сервер не отвечает / CORS заблокировал network 0
превышен таймаут timeout 0
отмена запроса (ушли со страницы) исходная ошибка пробрасывается как есть —
  1. 401 на запросе с токеном → вызывается unauthorizedHandler (его регистрирует useUnauthorizedRedirect): токен удаляется, сессия сбрасывается.
  2. 204 → undefined, иначе response.json().

Удобные методы: http.get/post/put/patch/delete.

api-error.ts — класс ApiError с геттерами isUnauthorized (401) и isClientError (4xx). Функция isApiError(e) — проверка типа.

token-storage.ts — токен в localStorage под ключом dz.accessToken. Все обращения обёрнуты в try/catch (приватный режим может запрещать хранилище). Вся работа с токеном только здесь: переход на httpOnly-cookie затронет один файл.

query-client.ts — настройки TanStack Query: - staleTime: 30_000 — данные свежие 30 секунд, повторный заход на страницу не дёргает сервер; - refetchOnWindowFocus: false — не перезапрашивать при возврате на вкладку; - retry: до 2 повторов, но не для 4xx (нет смысла повторять «не найдено»); - мутации не повторяются.

clients/ — клиенты бэкенда, по файлу на модуль API бэкенда (backend/app/modules/<x>). Файл содержит типы контракта и объект с функциями запросов. Сейчас:

authClient.register(payload)  // POST /auth/register, без токена
authClient.login(payload)     // POST /auth/login, без токена
authClient.me(signal)         // GET  /auth/me
authClient.logout()           // POST /auth/logout

Типы User, RegisterRequest, LoginRequest, TokenResponse реэкспортируются из @/shared/api. Новый модуль бэкенда → clients/<name>.ts + строка в clients/index.ts.

5.8. Модуль auth

api/session.ts — всё состояние сессии:

Хук Что делает
useSession() запрос ['auth','session'] с staleTime: Infinity. Нет токена → anonymous без запроса. Есть → GET /auth/me; 401 → токен удаляется, anonymous. Возвращает {status: 'loading' \| 'anonymous' \| 'authenticated', user}
useLogin() мутация входа; при успехе сохраняет токен и кладёт user в кэш сессии
useRegister() то же для регистрации (бэкенд сразу отдаёт токен)
useLogout() POST /logout (ошибку игнорирует), удаляет токен, очищает весь кэш запросов
useUnauthorizedRedirect() регистрирует обработчик 401 в http-клиенте; подключён один раз в AppProviders

Отдельного глобального стора (Redux, Zustand) нет: данные с сервера живут только в TanStack Query, сессия — это тоже просто запрос.

components/guards.tsx: - RequireAuth — пока сессия грузится, показывает спиннер; аноним → /login, с state.from = откуда пришёл (после входа вернёт туда же); - RedirectIfAuthenticated — вошедшего с /login и /register отправляет на /.

Формы (LoginForm, RegisterForm) построены одинаково: react-hook-form + zod-схема из model/schemas.ts. Тексты ошибок в схеме — ключи переводов ('validation.emailInvalid'), форма переводит их при выводе.

Форма Поля и проверки Ошибки сервера
Вход email (валидный), пароль (не пустой) 401 → «Неверный email или пароль», 403 → «Аккаунт отключён», сеть/таймаут → понятный текст
Регистрация имя и фамилия (1–100), email, пароль (8–128) — как на бэкенде 409 → ошибка у поля email, 422 → «Сервер не принял данные», прочее → общий текст

LoginPage показывает подсказку с демо-аккаунтом только при VITE_ENABLE_MOCKS=true.

5.9. Маршруты и раскладки

src/shared/config/routes.ts — все URL: path для роутера, to() для ссылок.

errorElement: RouteErrorPage          ← любая ошибка рендера/загрузки, 404 → NotFoundPage
├── RedirectIfAuthenticated
│   └── PublicLayout                  ← язык справа, логотип и слоган по центру
│       ├── /login     LoginPage
│       └── /register  RegisterPage
├── RequireAuth
│   └── AppLayout                     ← шапка: название, имя, язык, выход
│       └── /          HomePage       ← пока заглушка «Привет, {имя}!»
└── *                  NotFoundPage

appRoutes экспортируется отдельно от router, чтобы тесты могли собрать createMemoryRouter из тех же маршрутов.

5.10. Переводы (i18n)

  • Языки: румынский — эталон, русский. Один JSON на модуль (namespace): common, auth.
  • Ключи типизированы (i18next.d.ts берёт типы из румынских файлов): опечатка в ключе t('login.titel') — ошибка TypeScript. Русский файл типами не проверяется: если ключа нет, покажется румынский текст.
  • Определение языка: сначала localStorage['dz.locale'], потом язык браузера. Выбор сохраняется в localStorage. <html lang> обновляется при смене языка.
  • Множественное число — суффиксы i18next: _one/_few/_other (ro), _one/_few/_many/_other (ru).
  • Новый модуль: locales/{ro,ru}/<module>.json + строка в resources.ts.

5.11. Стили и UI

  • Tailwind CSS 4: настройка в src/index.css через @theme, файла tailwind.config.js нет.
  • Дизайн-токены — цвета в oklch: background, foreground, card, primary, secondary, muted, destructive, success, border, input, ring + радиусы. Компоненты используют только токены (bg-primary, text-muted-foreground), поэтому бренд и тёмная тема меняются в одном месте. Тема сейчас только светлая.
  • Шрифт объявлен как Inter, но сам файл шрифта не подключён — на машинах без Inter используется системный (см. 8).
  • Компоненты в shared/ui в стиле shadcn/ui: варианты через cva, классы через cn() (clsx + tailwind-merge: cn('px-2', 'px-4') → px-4). Button умеет asChild — отдать свои стили дочернему <Link>. FormField связывает <label>, поле и текст ошибки через id/aria-describedby.

5.12. Моки (MSW)

Одни и те же хендлеры (src/mocks/handlers) работают в двух местах:

Где Как подключено Поведение
Тесты src/test/setup.ts → msw/node setupServer onUnhandledRequest: 'error': запрос без мока роняет тест
Браузер VITE_ENABLE_MOCKS=true → src/mocks/browser.ts подменяет window.fetch; запросы к env.apiUrl отвечают хендлеры, без мока → 501, на бэкенд ничего не уходит; остальные запросы идут как обычно

Почему в браузере подмена fetch, а не Service Worker (стандартный способ MSW): в Brave Service Worker не перехватывал запросы, и они молча уходили на бэкенд. Подмена fetch работает везде. В консоли браузера каждый замоканный запрос пишется строкой [mocks].

data.ts — in-memory «база»: один демо-пользователь demo@example.md / password123. Хендлеры auth.ts повторяют поведение бэкенда (409 на занятый email, 401 на неверный пароль, токен вида mock-token-<id>). Данные живут до перезагрузки страницы.

5.13. Тесты

  • Vitest в окружении jsdom, конфиг vitest.config.ts наследует vite.config.ts (алиасы @/ работают), тесты — src/**/*.test.{ts,tsx} рядом с кодом.
  • setup.ts перед каждым тестом чистит localStorage и ставит румынский язык, после — размонтирует компоненты и сбрасывает переопределения хендлеров.
  • renderWithProviders(ui, { route }) — рендер с собственным QueryClient (без повторов) и MemoryRouter; возвращает user из @testing-library/user-event.
  • Сейчас 11 тестов: env.test.ts (4), LoginForm.test.tsx (4), RegisterForm.test.tsx (3).
  • Подмена ответа в конкретном тесте: server.use(http.post(apiPath('/auth/login'), () => HttpResponse.error())).

5.14. Качество кода

TypeScript (tsconfig.json → два проекта): - tsconfig.app.json — код в src/: strict, noUncheckedIndexedAccess (элемент массива может быть undefined), noUnusedLocals/Parameters, erasableSyntaxOnly (запрещены enum и namespace), verbatimModuleSyntax (типы импортируются через import type), алиас @/* → src/*; - tsconfig.node.json — vite.config.ts и vitest.config.ts (окружение Node).

ESLint (eslint.config.js, flat config): рекомендованные правила JS и TypeScript, React Hooks (включая правила React Compiler: например, запрет Date.now() прямо в рендере), React Refresh, плюс свои правила: - consistent-type-imports — типы только через import type; - неиспользуемые переменные — ошибка (кроме начинающихся с _); - архитектурные запреты no-restricted-imports из раздела 5.4; - последним идёт eslint-config-prettier — отключает правила, конфликтующие с Prettier.

Prettier (.prettierrc.json): без точек с запятой, одинарные кавычки, запятые в конце, ширина 100, плагин сортирует Tailwind-классы (в том числе внутри cn() и cva()). .prettierignore: dist, coverage, node_modules, mockServiceWorker.js, package-lock.json.

EditorConfig: UTF-8, LF, отступ 2 пробела (4 для .py), перевод строки в конце файла.

Husky + lint-staged: перед каждым коммитом .husky/pre-commit запускает npx lint-staged — только для изменённых файлов: *.ts,tsx → eslint --fix + prettier; *.json,css,md,html → prettier. Если ESLint находит неисправимую ошибку, коммит отменяется.

5.15. CI (.github/workflows/ci.yml)

Запускается на push в main и на каждый pull request. Ubuntu, Node 22, кэш npm: npm ci --ignore-scripts → typecheck → lint → format:check → test → build. Любой шаг упал — PR красный.

5.16. Docker и nginx

Dockerfile — две стадии: 1. build (node:22-alpine): npm ci, затем npm run build. Переменные VITE_* передаются как --build-arg, потому что вшиваются в бандл при сборке. 2. runtime (nginx:1.29-alpine): только dist/ и конфиг nginx, без Node.

nginx/default.conf.template (переменные подставляет envsubst при старте контейнера): - /api/ → проксируется на API_UPSTREAM (по умолчанию http://backend:8000); - /assets/ → кэш на год (immutable): в именах файлов хэш, новая версия = новое имя; - всё остальное → index.html с no-cache (SPA: маршруты обрабатывает React Router); - gzip для CSS, JS, JSON, SVG.

⚠️ Пока окружение одно (development), собранный образ тоже ходит на http://localhost:8000, и прокси /api/ в nginx не используется. Решить при появлении production (см. 8).

5.17. VS Code

Настройки есть в двух местах: DZHub/.vscode/ (когда открыта вся рабочая папка, пути с префиксом frontend/) и frontend/.vscode/ (когда открыт только фронтенд). - форматирование при сохранении (Prettier), ESLint-исправления при сохранении; - TypeScript из node_modules проекта; - Tailwind IntelliSense знает про index.css и функции cn/cva; - i18n Ally показывает переводы прямо в коде (эталон — ro).

Рекомендуемые расширения: ESLint, Prettier, Tailwind CSS, i18n Ally, Vitest, EditorConfig.


6. Бэкенд

6.1. Стек и зависимости (pyproject.toml)

Python 3.13 (.python-version), менеджер — uv (uv.lock фиксирует точные версии). [tool.uv] package = false — это приложение, а не библиотека.

Пакет Зачем
fastapi веб-фреймворк, OpenAPI-документация
uvicorn[standard] ASGI-сервер
sqlalchemy[asyncio] 2.1 + asyncpg ORM и асинхронный драйвер PostgreSQL
alembic миграции схемы базы
pydantic[email], pydantic-settings схемы запросов/ответов, настройки из .env
pwdlib[argon2] хеширование паролей (argon2id)
pyjwt JWT-токены
dev: pytest, pytest-asyncio, httpx тесты
dev: ruff, mypy линтер/форматтер, проверка типов

6.2. Структура

backend/
├── app/
│   ├── main.py              ← create_app(): логи, CORS, middleware, роутеры
│   ├── models.py            ← реестр моделей для Alembic
│   ├── api/
│   │   ├── router.py        ← /api/v1: подключение роутеров модулей
│   │   └── health.py        ← /health и /health/ready
│   ├── core/
│   │   ├── config.py        ← Settings из .env
│   │   ├── db.py            ← движок, сессии, Base, TimestampMixin
│   │   ├── logging.py       ← форматы логов, request_id
│   │   ├── middleware.py    ← X-Request-ID и лог каждого запроса
│   │   └── schemas.py       ← CamelModel
│   └── modules/
│       ├── users/           ← модель User, UserPublic, поиск
│       └── auth/            ← регистрация, вход, токены, CurrentUser
├── alembic/                 ← env.py + versions/ (миграции)
├── tests/                   ← conftest.py, test_auth.py, test_health.py
├── Dockerfile, docker-compose.yml, Makefile
└── pyproject.toml, uv.lock, alembic.ini, .env, .env.example

Модуль (app/modules/<name>/) всегда устроен одинаково:

Файл Что внутри Правило
models.py таблицы SQLAlchemy импортировать в app/models.py, иначе Alembic не увидит
schemas.py Pydantic-схемы (наследуют CamelModel) —
service.py бизнес-логика не знает про HTTP: ошибки — свои исключения
router.py эндпоинты проверяет вход, зовёт сервис, переводит исключения в HTTP-коды

6.3. Настройки (.env → app/core/config.py)

Settings (pydantic-settings) читает переменные окружения и файл .env. В коде — только get_settings() (закэширован через lru_cache).

Переменная По умолчанию Что делает
ENVIRONMENT local local / test / production; в production проверяется секрет JWT
LOG_LEVEL INFO DEBUG / INFO / WARNING / ERROR
LOG_FORMAT text text — для глаз, json — для систем сбора логов
POSTGRES_HOST / PORT localhost / 5433 адрес базы (в Docker переопределяется на db:5432)
POSTGRES_USER / PASSWORD / DB dzhub ×3 учётка и имя базы; те же значения берёт контейнер db
DB_ECHO false печатать каждый SQL-запрос
CORS_ORIGINS [] (в вашем .env: ["http://localhost:5173"]) JSON-список разрешённых источников
JWT_SECRET_KEY dev-заглушка секрет подписи токенов; в production обязателен свой, ≥ 32 символа, иначе приложение не запустится
ACCESS_TOKEN_EXPIRE_MINUTES 10080 (неделя) срок жизни токена

jwt_algorithm = HS256 и app_name = "DZ Hub API" задаются в коде.

6.4. База данных

app/core/db.py: - engine — асинхронный движок с pool_pre_ping=True (проверяет соединение перед использованием, переживает перезапуск базы); - get_session() / SessionDep — одна сессия на запрос. Коммит делает сервис, а не роутер и не зависимость; - Base с NAMING_CONVENTION — предсказуемые имена ограничений (pk_users, uq_users_email, ck_users_email_lowercase…), чтобы Alembic мог найти их и удалить; - TimestampMixin — created_at / updated_at, значения ставит сама база (now()).

Таблица users (единственная сейчас):

Колонка Тип Особенности
id UUID uuidv7() на стороне базы — требует PostgreSQL 18. UUIDv7 упорядочены по времени, индексу так легче
email varchar(320) уникальный; CHECK email = lower(email) — хранится только в нижнем регистре
password_hash varchar(255) argon2id
first_name, last_name varchar(100)
is_active bool, default true false → вход даёт 403, токен перестаёт работать
created_at, updated_at timestamptz

Модель не привязана к аэродрому: аккаунт один на все аэродромы портала. Свойство full_name вычисляется в Python, в базе его нет.

Миграции (Alembic): - адрес базы alembic/env.py берёт из get_settings(), а не из alembic.ini; - compare_type=True — autogenerate замечает смену типов колонок; - имена файлов: 2026_10_02_2152-<rev>_<slug>.py; после генерации файл автоматически прогоняется через ruff --fix и ruff format (post_write_hooks); - сейчас одна миграция: b81f8147cc9f_create_users; - порядок работы: поменять модель → make makemigration m="..." → прочитать сгенерированный файл → make migrate.

6.5. Авторизация (modules/auth)

Пароли (security.py): PasswordHash.recommended() = argon2id. Если пользователь с таким email не найден, всё равно проверяется «пустой» хеш (_DUMMY_HASH): время ответа одинаковое, и по нему нельзя понять, зарегистрирован ли email.

Токены: JWT HS256 с полями sub (id пользователя), iat, exp. При проверке sub и exp обязательны. Подделанный, просроченный или битый токен → None → 401.

CurrentUser (dependencies.py) — зависимость для защищённых эндпоинтов: нет заголовка / плохой токен / пользователь удалён или отключён → 401 с WWW-Authenticate: Bearer. Использование: параметр user: CurrentUser.

Сервис (service.py): - register: email нормализуется (strip().lower()), проверяется занятость, пароль хешируется, коммит. Если параллельный запрос успел занять тот же email, ловится IntegrityError → тоже EmailAlreadyRegisteredError → 409; - authenticate: неверный email и неверный пароль дают одинаковый ответ 401; отключённый пользователь → 403.

6.6. Логи и request_id

  • Каждый запрос получает request_id: берётся из заголовка X-Request-ID или генерируется (12 hex-символов), возвращается в ответе и попадает в каждую строку лога этого запроса (через contextvars). Ошибку из браузера можно найти в логах по этому id.
  • Формат text: 08:15:02 INFO app.request [a1b2c3d4e5f6] POST /api/v1/auth/login 200 duration_ms=12.3. Формат json: одна JSON-строка на запись, для Loki/ELK.
  • Логи uvicorn направлены в общий обработчик; его access-лог отключён: запросы пишет своя middleware.
  • Уровень строки о запросе: 5xx → WARNING; успешные /health → DEBUG (healthcheck не засоряет лог); остальное → INFO. Необработанное исключение → exception со стеком.
  • Свои поля: logger.info("Взлёт создан", extra={"load_id": load.id}).

6.7. Здоровье сервиса

  • GET /api/v1/health — процесс жив (база не проверяется). Его дёргает HEALTHCHECK в Dockerfile.
  • GET /api/v1/health/ready — SELECT 1 в базу; недоступна → 503.

6.8. Тесты

  • pytest + pytest-asyncio (asyncio_mode = auto, один event loop на сессию).
  • Отдельная база dzhub_test: создаётся автоматически, схема пересоздаётся при каждом запуске (drop_all + create_all, не через миграции).
  • Каждый тест в транзакции, которая откатывается: commit() в коде приложения превращается в SAVEPOINT, тесты не видят данных друг друга, рабочая база не затрагивается.
  • HTTP-клиент — httpx.AsyncClient с ASGITransport (без настоящего сервера), сессия подменяется через app.dependency_overrides.
  • 16 тестов: health (4) и auth (12): регистрация, регистронезависимый дубль email, валидация, хеширование пароля, вход, неверный пароль, неизвестный email выглядит как неверный пароль, отключённый пользователь, /me с токеном/без/с битым, выход.
  • Нужна запущенная база: make db.

6.9. Docker, Compose, Makefile

docker-compose.yml (проект dzhub): - db — postgres:18-alpine, порт 5433→5432, данные в томе pgdata, healthcheck через pg_isready; - api — собирается из Dockerfile, читает .env, но POSTGRES_HOST=db, POSTGRES_PORT=5432; стартует только после того, как db здоров. Команда: alembic upgrade head && uvicorn ... --reload. Папки app/ и alembic/ смонтированы внутрь, поэтому правки кода подхватываются без пересборки.

Dockerfile — две стадии: builder ставит зависимости через uv (с кэшем, без dev-зависимостей, сначала только lock-файл — для кэша слоёв), финальный образ python:3.13-slim запускается не от root (пользователь app), с HEALTHCHECK.

Makefile (make help — список):

Команда Что делает
make install uv sync + .env из .env.example, если его нет
make dev API локально с автоперезагрузкой (база — через make db)
make db только PostgreSQL в Docker
make up / down / restart база + API в Docker / остановить / перезапустить
make logs, ps, psql логи, статус контейнеров, консоль PostgreSQL
make migrate / downgrade / history применить / откатить последнюю / история миграций
make makemigration m="..." сгенерировать миграцию по изменениям моделей
make test pytest
make lint / format / typecheck ruff (проверка) / ruff (исправление) / mypy
make check lint + typecheck + test
make clean ⚠️ удалить контейнеры и данные базы

6.10. Качество кода

  • ruff: ширина 100, Python 3.13, правила E, W, F, I (порядок импортов), B, UP, SIM, ASYNC, RUF; отключены RUF001–003 (иначе ругается на кириллицу в строках и комментариях).
  • mypy: strict = true + плагин pydantic; папка миграций исключена.

7. Git и ветки

Фронтенд (frontend/, remote origin):

Ветка Что там
main каркас + «changed app to dz hub» (5526efe, перенесён cherry-pick'ом)
create-auth-flow-and-connect-with-auth-api текущая: main + подключение настоящего API (e3cc09a)
try/associations мок-прототип всего MVP: дни, взлёты, запись, допуски, самолёты, кабинет. Только на моках, роли пользователя (организатор/парашютист) на бэкенде нет

Коммит «changed app to dz hub» существует дважды с разными хэшами (9d454a0 в try/associations и 5526efe в main) — следствие cherry-pick. При слиянии git обычно разберётся сам; rebase try/associations на main уберёт дубль.

Бэкенд в git ещё не добавлен: правки в backend/ (например, CORS) нигде не сохранены, кроме диска.


8. Известные недоделки и долги

Что Почему важно Что сделать
Бэкенд не в git нет истории и резервной копии git init, первый коммит, remote
Нет production-окружения Docker-образ фронтенда ходит на localhost:8000 добавить production в backendUrls (вероятно, относительный '' → запросы на тот же домен через nginx /api/) и --build-arg VITE_APP_ENV=production
Токен в localStorage, не отзывается при XSS токен можно украсть; после выхода он валиден ещё до 7 дней позже: httpOnly-cookie (меняется только token-storage.ts + бэкенд) и/или чёрный список токенов
Типы API пишутся вручную могут разъехаться с бэкендом генерация из /api/v1/openapi.json (например, openapi-typescript)
public/mockServiceWorker.js и "msw.workerDirectory" в package.json остались от Service Worker-моков, сейчас не используются можно удалить
Шрифт Inter не подключён выглядит по-разному на разных машинах подключить файл шрифта или убрать из --font-sans
README.md фронтенда: образ ham-frontend в разделе Docker имя от другого проекта переименовать в dz-hub-frontend
Корневой README.md: бэкенд «не начат» устарело обновить таблицу
Нет ролей и допусков на бэкенде прототип try/associations на них опирается следующие модули бэкенда по MVP.md
Тестовая база создаётся через create_all, не через миграции ошибку в миграции тесты не поймают позже прогонять alembic upgrade head в тестах или в CI
Нет CI для бэкенда make check никто не запускает автоматически GitHub Actions с сервисом PostgreSQL
HomePage — заглушка — заменить первым настоящим экраном

9. Шпаргалка

Добавить модуль от начала до конца (на примере «прыжковые дни»):

  1. Бэкенд: app/modules/jump_days/{models,schemas,service,router}.py → импорт модели в app/models.py → роутер в app/api/router.py → make makemigration m="add jump days" → проверить файл → make migrate → тесты в tests/.
  2. Фронтенд, клиент: src/shared/api/clients/jump-days.ts (типы + запросы) → экспорт в clients/index.ts.
  3. Фронтенд, модуль: src/features/jump-days/{api,model,components,pages} + index.ts.
  4. Маршрут: URL в shared/config/routes.ts, страница в app/router.tsx.
  5. Переводы: i18n/locales/{ro,ru}/jumpDays.json + i18n/resources.ts.
  6. Моки: mocks/handlers/jump-days.ts + handlers/index.ts (нужны для тестов).

Частые ситуации:

Симптом Причина и решение
На экране красный текст «Некорректная конфигурация окружения» проверьте .env* и .env.local: какое поле названо в ошибке
«Нет связи с сервером» на фронте бэкенд не запущен (make up) или CORS: проверьте CORS_ORIGINS и что фронт на 5173
Vite не стартует: порт занят закрыть другой npm run dev (порт фиксирован намеренно)
Поменяли backend/.env, а эффекта нет docker compose up -d --force-recreate api
Хочу работать без бэкенда VITE_ENABLE_MOCKS=true в frontend/.env.local, вход demo@example.md / password123
Коммит отменился сам lint-staged нашёл ошибку ESLint: исправить и закоммитить снова
Ищу ошибку запроса в логах бэкенда взять X-Request-ID из ответа (DevTools → Network) и найти его в make logs
Документация API http://localhost:8000/api/docs