# Слой src — основные данные > Таблицы слоя `src`; правила — d-db-2, d-db-3 (`std1\_rules\d\d-db.md`). > 🛡 Все функции слоя принимают `p_athlete_id`; доступ — только в пределах > атлета (изоляция, requirements Ф-8.5). ## Перечень таблиц | Таблица | Тип | Набор функций | Функции | Комментарий | |---|---|---|---|---| | `src.athlete` | сущность | athlete_insert, athlete_update, athlete_select, athlete_get | CUSTOM | без `_delete`: блокировка вместо удаления (`is_blocked`); запись/чтение — владелец и сервисы сервера, не атлет | | `src.athlete_profile` | сущность | athlete_profile_upsert, athlete_profile_get | CUSTOM | 1:1 к `athlete`; upsert вместо insert/update (одна запись) | | `src.exercise` | сущность | exercise_insert, exercise_update, exercise_delete, exercise_select, exercise_get | CRUD | `_delete` — логический (`is_active=0`, requirements Ф-4.2); функции фильтруют `is_active` по умолчанию | | `src.exercise_replacement` | связь ММ | exercise_replacement_insert, exercise_replacement_delete | MM | типовой набор связи ММ — d-db-3; пара-замена для правила 6 (документ метрик); выборка — внутри `exercise_select` | | `src.program` | сущность | program_insert, program_select, program_get | CUSTOM | контейнер версий; без update/delete — жизненный цикл ведут версии | | `src.program_version` | сущность | program_version_insert, program_version_update, program_version_select, program_version_get | CRUD | `body` jsonb (схема дизайна LLM-планирования разд. 2); 🛡 неизменяемость: `_update` меняет только `status` | | `src.workout` | сущность | workout_insert, workout_update, workout_select, workout_get | CRUD | PK UUID — идемпотентность (Р2-Б); `facts_json` — факты .fit | | `src.workout_set` | сущность | workout_set_insert, workout_select (в составе), workout_set_get | CUSTOM | выборка — агрегатом в `workout_select` (подходы в карточке); `_delete`/`_update` нет: правка = новая пометка `source=edit` (история сохраняется) | | `src.command_log` | сущность | command_log_insert, command_log_select | CUSTOM | журнал голосовых команд (Ф-1.5, 🛡 не редактируется); update/delete нет | | `src.daily_log` | сущность | daily_log_upsert, daily_log_select | CUSTOM | день уникален для атлета (`UNIQUE(athlete_id, date)`) — upsert | | `src.workout_metrics` | сущность | workout_metrics_upsert, workout_metrics_select | CUSTOM | 1:1 к `workout`, считает сервер (MetricsEngine) — телефон только читает; upsert (пересчёт) | | `src.breaks` | сущность | break_insert, break_update, break_select | CUSTOM | перерывы (Ф-14); закрытие = update (`ended_on`); delete нет — журнал | | `src.migration` | служебная | migration_insert, migration_select | CUSTOM | журнал применённых миграций (d-db-1); пишет `--mode migrate` | > Новая редакция (2026-10-17, ревью №2): FK-колонки приведены к uuid > (Р2-Б доведён, З-16); `src.breaks` — `planned_return`, `source` = auto|manual > (З-17); `src.migration` — журнал миграций (З-25). УТВЕРЖДЕНО > (2026-10-17). Правки ревью №3: `src.program.external_id`, примечание > `body` (З-31), `is_active=false` (З-34) — УТВЕРЖДЕНО (2026-10-17). ## Описания ### `src.athlete` — атлет | Поле | Тип | Описание | |---|---|---| | `id` | serial PK | — | | `name` | text NOT NULL | отображаемое имя (AdminWeb, Ф-9.1) | | `api_key_hash` | text NOT NULL UNIQUE | SHA-256 ключа (ключ в открытом виде не хранится; сопоставление запроса — по хэшу) | | `is_blocked` | boolean NOT NULL DEFAULT false | блокировка ключа → `401` (Ф-9.1) | | `created_at` | timestamptz NOT NULL DEFAULT now() | — | ### `src.athlete_profile` — профиль атлета (Ф-6, 1:1 к `athlete`) | Поле | Тип | Описание | |---|---|---| | `id` | serial PK | — | | `athlete_id` | int NOT NULL UNIQUE | → `src.athlete.id` | | `sex` | text | `m` / `f` | | `birth_year` | int | год рождения (Танака: 208 − 0,7×возраст) | | `weight_kg` | numeric(5,1) | вес тела (последний введённый) | | `hr_max` | int NULL | ручной ввод; NULL → расчёт по Танаке (ОВ-1) | | `updated_at` | timestamptz NOT NULL | — | ### `src.exercise` — упражнение библиотеки атлета (Ф-4) | Поле | Тип | Описание | |---|---|---| | `id` | uuid PK DEFAULT gen_random_uuid() | 🛡 неизменяемый `exercise_id` (Ф-4.3); UUID генерирует телефон (Р2-Б) | | `athlete_id` | int NOT NULL | → `src.athlete.id`; 🛡 библиотека принадлежит атлету (Ф-4.4) | | `name` | text NOT NULL | название; `UNIQUE(athlete_id, name)` | | `group_id` | int NOT NULL | → `ref.exercise_group.id` | | `is_base` | boolean NOT NULL DEFAULT true | базовое/изоляция (шаг прогрессии, Ф-12.2) | | `is_active` | boolean NOT NULL DEFAULT true | логическое удаление — `is_active=false` (Ф-4.2, З-34) | ### `src.exercise_replacement` — пара-замена (ММ, правило 6) | Поле | Тип | Описание | |---|---|---| | `exercise_id` | uuid | → `src.exercise.id` | | `replacement_id` | uuid | → `src.exercise.id`; `CHECK (exercise_id <> replacement_id)`; `PK (exercise_id, replacement_id)` | Замена допустима только внутри группы — проверяет приложение (группы у обоих равны); БД фиксирует пары. ### `src.program` — программа атлета (Ф-10) | Поле | Тип | Описание | |---|---|---| | `id` | serial PK | — | | `athlete_id` | int NOT NULL | → `src.athlete.id`; `UNIQUE(athlete_id)` — одна программа на атлета (версии — в `program_version`) | | `title` | text NOT NULL | название («Программа v4») | | `external_id` | text UNIQUE NULL | идентификатор пакета (`program_id` из program-import.json, З-31) | | `created_at` | timestamptz NOT NULL DEFAULT now() | — | ### `src.program_version` — неизменяемая версия программы (Ф-10, 🛡) | Поле | Тип | Описание | |---|---|---| | `id` | uuid PK DEFAULT gen_random_uuid() | `program_version_id` для тренировок; UUID — телефон (Р2-Б) | | `program_id` | int NOT NULL | → `src.program.id` | | `version` | int NOT NULL | `UNIQUE(program_id, version)`; 1, 2, 3… | | `based_on_version` | int NULL | предыдущая версия (NULL — первая) | | `status` | text NOT NULL DEFAULT 'draft' | `draft` \| `active` \| `archived`; 🛡 `active` — ровно одна на программу (частичный уникальный индекс `WHERE status='active'`) | | `author` | text NOT NULL | `user` \| `admin` \| `rules` \| `llm` (v2) | | `body` | jsonb NOT NULL | недели → тренировки → упражнения (`exercise_id`) → подходы-план; схема — дизайн LLM-планирования разд. 2; секция `profile` пакета (program-import.json) в body не переносится (v1, З-31) | | `rationale` | text | «почему» — журнал изменений, показывается пользователю | | `created_at` | timestamptz NOT NULL DEFAULT now() | — | ### `src.workout` — тренировка (телефон + факт .fit) | Поле | Тип | Описание | |---|---|---| | `id` | uuid PK | UUID — телефон (строки «только .fit» — сервер); идемпотентность (Ф-7.4, Р2-Б) | | `athlete_id` | int NOT NULL | → `src.athlete.id` | | `program_version_id` | uuid NULL | → `src.program_version.id`; версия, активная на начало (Ф-10.3) | | `started_at` | timestamptz NOT NULL | начало (UTC) | | `ended_at` | timestamptz NULL | конец | | `status` | text NOT NULL DEFAULT 'active' | `active` \| `finished` \| `cancelled` (Ф-3.5) | | `session_kind` | text NOT NULL DEFAULT 'normal' | `normal` \| `return` \| `deload` \| `test` — интерпретация снижения весов (Ф-14.5) | | `avg_hr` | int NULL | средний пульс | | `facts_json` | jsonb NULL | канонический факт из .fit (`workout-facts/1`, tech-requirements 2.3); заполняет FitParser | | `fit_file_id` | text NULL | дедупликация fetch (Ф-8.1); уникальность — составная `UNIQUE(athlete_id, fit_file_id)` | | `hr_recovery_s` | int NULL | возврат пульса к <90 (Ф-13.4, из .fit) | `UNIQUE(athlete_id, fit_file_id)` — единственная уникальность `fit_file_id`; сопоставление телефон ↔ .fit — окно ±15 минут по `started_at` (Ф-8.4): факты пишутся в ту же строку (`facts_json`); если тренировки с телефона не было — создаётся строка «только .fit» с UUID, который генерирует сервер (Р2-Б). ### `src.workout_set` — подход (телефон) | Поле | Тип | Описание | |---|---|---| | `id` | uuid PK | UUID — телефон (Р2-Б) | | `workout_id` | uuid NOT NULL | → `src.workout.id` | | `athlete_id` | int NOT NULL | денормализация для изоляции выборок | | `exercise_id` | uuid NULL | → `src.exercise.id`; NULL — упражнение не выбрано | | `seq` | int NOT NULL | порядковый номер подхода в тренировке | | `weight_kg` | numeric(6,2) NOT NULL | вес | | `reps` | int NOT NULL | повторения | | `done_at` | timestamptz NOT NULL | момент фиксации | | `source` | text NOT NULL | `voice` \| `watch` \| `edit` | | `in_plan` | boolean NOT NULL | «по плану» (Ф-11.2) | | `rpe` | int NULL CHECK (rpe BETWEEN 1 AND 10) | субъективная нагрузка (Ф-13.3) | | `knee_pain` | int NULL CHECK (0–10) | боль в колене (группа `legs`, Ф-13.3) | ### `src.command_log` — журнал голосовых команд (Ф-1.5, 🛡) | Поле | Тип | Описание | |---|---|---| | `id` | uuid PK | UUID — телефон (Р2-Б) | | `workout_id` | uuid NOT NULL | → `src.workout.id` | | `t_offset_s` | int NOT NULL | секунды от начала тренировки (сопоставление с .fit) | | `recognized_text` | text NOT NULL | распознанный текст (диагностика STT) | | `semantics` | jsonb NOT NULL | `{cmd, weight, reps, rest_sec, rpe}` | | `accepted` | boolean NOT NULL | принята грамматикой/диапазоном | ### `src.daily_log` — дневник самоконтроля (Ф-13.1) | Поле | Тип | Описание | |---|---|---| | `id` | uuid PK | UUID — телефон (Р2-Б) | | `athlete_id` | int NOT NULL | → `src.athlete.id`; `UNIQUE(athlete_id, date)` | | `date` | date NOT NULL | день (локальная дата атлета) | | `bp_sys` | int NULL | систолическое АД | | `bp_dias` | int NULL | диастолическое АД | | `weight_kg` | numeric(5,1) NULL | вес тела | | `hr_resting` | int NULL | пульс покоя, утро (Р1-А — поле v1, данные правила 1) | | `waist_cm` | numeric(5,1) NULL | талия (v2) | | `sleep_h` | numeric(3,1) NULL | сон, часов (v2) | | `wellbeing` | int NULL CHECK (1–5) | самочувствие 1–5 (Р1-А — поле v1, данные правила 1) | ### `src.breaks` — перерывы (Ф-14) | Поле | Тип | Описание | |---|---|---| | `id` | uuid PK | UUID — телефон/сервер (Р2-Б) | | `athlete_id` | int NOT NULL | → `src.athlete.id` | | `kind` | text NULL | ответ атлета: `illness` \| `vacation` \| `workload` \| `injury` \| `other`; NULL — ещё не отвечен (правила 12–14 ждут ответа) | | `source` | text NOT NULL DEFAULT 'auto' | `auto` — сервер по пропускам (Ф-14.1); `manual` — запланированный простой, объявленный атлетом заранее с телефона (Ф-14.2). Из sync принимается только `manual`; `auto` проставляет сервер | | `started_on` | date NOT NULL | первый пропущенный день | | `planned_return` | date NULL | предполагаемая дата возврата (ответ атлета, Ф-14.2) | | `ended_on` | date NULL | возврат (заполняется первой сессией после break) | | `note` | text | причина-текст пользователя | ### `src.workout_metrics` — метрики тренировки (MetricsEngine, 1:1) | Поле | Тип | Описание | |---|---|---| | `id` | serial PK | — | | `workout_id` | uuid NOT NULL UNIQUE | → `src.workout.id` | | `athlete_id` | int NOT NULL | изоляция выборок | | `adherence` | numeric(4,1) | % закрытых подходов плана (КУ-5) | | `tonnage` | numeric(9,1) | Σ вес×повторы, кг | | `tonnage_by_group` | jsonb | `{"push": 4200, …}` | | `e1pm_by_exercise` | jsonb | `{"": 82.5}` (Эпли, ОВ-7) | | `hr_metrics` | jsonb | средний/пиковый пульс подхода, отдых между подходами | | `red_flags` | jsonb | сработавшие красные флаги (АД, пульс, колено — документ метрик) | | `computed_at` | timestamptz NOT NULL | — | ### `src.migration` — журнал применённых миграций (служебная, d-db-1) | Поле | Тип | Описание | |---|---|---| | `id` | serial PK | — | | `filename` | text NOT NULL UNIQUE | имя применённого psql-файла из `2-db\migrations\` | | `applied_at` | timestamptz NOT NULL DEFAULT now() | момент применения |