# Требования к модулю: m02-rest-api > Модуль — REST API сервера v1: проект `Server.WatchBody.Web` > (ASP.NET Core, Kestrel — только localhost), middleware ключа атлета, > `POST /api/v1/sync`, чтение тренировок и активной программы. > Самодостаточная копия требований (d-dev-6): внешние документы > продублированы по содержанию, ссылки — только для контекста. ## 1. Назначение и границы **В границах модуля:** - проект `Server.WatchBody.Web` (net10.0, ASP.NET Core MVC) — каркас, конфигурация (`appsettings.json`, валидация при старте — tech-requirements 4.2: `Api.*`, `Db.ConnectionString` — фатально при неудаче), запуск по HTTPS-прокси снаружи (сам Kestrel — localhost); таблица маршрутов — fluent `Kit.Helpers.Routes` (`Config\RouteConfig.cs`, паттерн — Kit.Task.WebAp\Task.WebApp), обработчики — контроллеры `Controllers\` (решение владельца 2026-10-18); - сопоставление атлетов конфига `Api.Athletes` (`{name, apiKey}`) с `src.athlete` при старте: upsert по SHA-256-хэшу ключа (идемпотентно; запись в БД — источник `is_blocked`); - middleware аутентификации `X-Api-Key` (см. разд. 3); - `POST /api/v1/sync` — приём пакета записей телефона (разд. 4); - `GET /api/v1/workouts`, `GET /api/v1/workouts/{id}`, `GET /api/v1/programs/active` (разд. 5); - интеграционные тесты эндпоинтов (WebApplicationFactory + тестовая БД). **Вне границ модуля:** AdminWeb (m08: admin REST API `/api/admin/*` в этом же проекте + UI-приложение Flutter Web `3-app\3-web\admin-web` — решение владельца 2026-10-17), fetch/import .fit (m06, m07), метрики/правила (m04, m05), ProgramStore как сервис правил (m03; здесь — только запись версии из sync), всё на телефоне и часах. Доступ к БД — только DAL-репозитории модуля m01 (`Server.WatchBody.Core`); прямой SQL в Web-проекте запрещён. ## 2. Стек и соглашения - C# / .NET 10, ASP.NET Core (minimal API), `Server.WatchBody.Core` (домен + репозитории m01) и Kit.Helpers (`Log.Logger` — файловые логи, уровень Info). - Конфигурация — `appsettings.json` (`Api.Key`, `Api.Athletes[]`, `Db.ConnectionString`); валидация при старте, ошибки — в лог и stderr, код возврата 1. - Идентификация атлета — по хэшу ключа через `AthleteRepository` (`athlete_select` по `p_api_key_hash`); middleware кладёт `athlete_id` в `HttpContext.Items`. - Формат ответов — JSON (System.Text.Json, camelCase); ошибки — `{ "error": "<текст>" }` с соответствующим кодом HTTP. ## 3. Аутентификация (requirements Ф-8.5, Ф-7.2 — 🛡) - Каждый запрос (кроме health-check `GET /health`) обязан нести заголовок `X-Api-Key`. - Сервер считает SHA-256 ключа и ищет атлета в `src.athlete` по `api_key_hash`. Ключ не найден или `is_blocked = true` → `401` (тело: `{"error": "unauthorized"}`), запрос не доходит до обработчиков. - 🛡 Атлета определяет только ключ: телефон не передаёт идентификатор атлета в данных пакета; `athlete_id` для всех записей пакета берётся из middleware (изоляция — каждый запрос видит данные только своего атлета). ## 4. `POST /api/v1/sync` (Ф-7.3, tech-requirements 2.1–2.2) Запрос — JSON-массив записей (пакет ≤ 50), каждая: ```json { "client_guid": "uuid-v4", "entity": "workout | workout_set | exercise | program_version | profile | daily_log | break", "op": "upsert", "data": { "...схема сущности..." } } ``` **Ключевые поля `data`** (tech-requirements 2.2): - `exercise`: `exercise_id` (uuid), `name`, `group` (push|pull|legs|core|cardio), `is_base`, `is_active`; повторная отправка упражнения с `is_active=false` — **логическое удаление** (`src.exercise_delete`, З-41(а): единственный серверный путь установки флага в v1; идемпотентно, GUID включается в `accepted` в обоих случаях, Ф-7.4); - `workout`: `workout_id` (uuid), `started_at`/`ended_at` (UTC ISO 8601), `status` (active|finished|cancelled), `session_kind` (normal|return|deload|test), `program_version_id` (nullable), `avg_hr`; - `workout_set`: `workout_id`, `exercise_id`, `seq`, `weight_kg`, `reps`, `done_at`, `source` (voice|watch|edit), `in_plan`, `rpe` (int? 1–10), `knee_pain` (int? 0–10, только «ноги»); - `profile`: `sex`, `birth_year`, `weight_kg`, `hr_max` (nullable); - `break`: `kind` (nullable), `source` (телефон передаёт только `manual`), `started_on`, `planned_return`, `note`; - `daily_log`: `date` (YYYY-MM-DD), `bp_sys`, `bp_dias`, `weight_kg`, `hr_resting`, `wellbeing` (1–5); - `program_version`: JSON-схема программы из дизайна LLM-планирования (разд. 2) — пишется в `body` как есть; `athlete_id` в теле не передаётся (выводится из ключа). **Правила обработки:** 1. Пакет атомарен — «все или ничего»: одна транзакция на пакет; при любой ошибке записи пакет целиком отклоняется. 2. Успех → `200 { "accepted": ["guid1", ...] }` — все GUID пакета. 3. 🛡 Идемпотентность: повтор пакета безопасен — записи по `client_guid` (PK uuid) уже существуют, `workout_insert` и insert-функции uuid- сущностей возвращают существующий id; сервер снова отвечает тем же `accepted` (ответ идемпотентен). 4. `409` — конфликт версии программы (`UNIQUE(program_id, version)` в `program_version_insert`); телефон перечитывает программу и перезаписывает локально. Тело: `{"error": "program_version_conflict"}`. 5. `break` с `source = "auto"` от телефона отклоняется (`400`) — `auto` проставляет только сервер (Ф-14.1); аналогично прочие нарушения схем/диапазонов (`rpe` 1–10, `wellbeing` 1–5, пакет > 50, неизвестная `entity`) — `400` с текстом ошибки. 6. Маппинг записей на функции БД (репозитории m01): workout → `workout_insert`/`workout_update` (update — при `ended_at`/`status`/`avg_hr`); workout_set → `workout_set_insert`; exercise → `exercise_insert`; program_version → `program_version_insert` (при первой версии атлета — `program_insert`); profile → `athlete_profile_upsert`; daily_log → `daily_log_upsert`; break → `break_insert` (`source='manual'`). ## 5. Чтение (Ф-8.5) | Эндпоинт | Ответ | Реализация | |---|---|---| | `GET /api/v1/workouts?limit=&offset=` | массив карточек тренировок (с подходами), пагинация (limit ≤ 100, умолчание 20) | `workout_select` (карточки + sets агрегатом) | | `GET /api/v1/workouts/{id}` | полная карточка тренировки + command_log | `workout_get`; чужой id → `404` | | `GET /api/v1/programs/active` | активная версия программы атлета (с `body`) или `404` | `program_select` + `program_version_select (status=active)` + `program_version_get` | Все выборки — в пределах `athlete_id` из middleware (🛡 изоляция). ## 6. Инварианты (🛡 — нарушать запрещено) 1. Изоляция: каждый запрос видит и пишет данные только своего атлета (атлет — по `X-Api-Key`; в теле пакета атлет не передаётся). 2. Идемпотентность: повтор пакета не создаёт дублей, ответ повторения — тот же `accepted`. 3. Атомарность пакета: «все или ничего». 4. API не редактирует факт-данные в обход sync: ручного ввода на сервере нет (Ф-8.6); пишет только через репозитории m01. 5. `break.source = 'auto'` из sync не принимается. ## 7. Критерии приёмки модуля 1. Без ключа / с чужим / заблокированным ключом — `401` на все эндпоинты; изоляция подтверждена тестом (атлет Б не видит данные атлета А, запись под чужой `workout_id` отклоняется). 2. Пакет sync: успешный приём возвращает все GUID; повтор пакета — тот же ответ, дублей в БД нет; конфликт версии программы — `409`; нарушение схемы — `400`. 3. `GET workouts` (пагинация), `GET workouts/{id}` (карточка, чужой id → 404), `GET programs/active` (или 404) работают в пределах атлета. 4. Тесты эндпоинтов зелёные; валидация конфига при старте (пустой `Api.Key`, дубликаты ключей атлетов — код возврата 1).