# Технические требования: WatchBody («Феникс 6X») v1 > Шаг 5 процесса разработки (`std1\_process\process.md`). > Источники: [vision.md](../1-reqs/vision.md) v1.5, > [requirements.md](../1-reqs/requirements.md) (утверждён, раздел 7 — > открытые вопросы), [architecture.md](../2-arch/architecture.md) > (утверждена), дизайн LLM-планирования, документ форматов, > UI/UX-рекомендации ([ui-ux-recommendations.md](ui-ux-recommendations.md)). > Версия 1.1 — **УТВЕРЖДЕНО** (2026-10-17; интеграция документа метрик — > Ф-13, Safety-пороги, daily_log — утверждена вместе с документом). > Версия 1.2 — черновик (2026-10-17): errata ревью (З-6, З-11) + > интеграция документа перерывов (Ф-14: сущность `break`, > `session_kind`). > Версия 1.3 — черновик (2026-10-17): волна 2 — Р1-А (daily_log v1 > поля), Р2-Б (UUID PK клиентских сущностей, маппинг id не нужен), > Р3-А (Safety на телефоне), З-7 (RPE 1–10); UI/UX-стандарт > (`ui-ux-recommendations.md`). Утверждение — Пользователь! > Версия 1.4 — **УТВЕРЖДЕНО** (2026-10-17): ревью №2 > (`0-mng\review-2026-10-17-2.md`) — сущность `break` согласована с > `src.breaks` (З-17), ОВ-7 — таргет `fenix6x` 280×280 (З-18), > ОВ-10 закрыт (З-19, О-1-А), `Safety.HrStrengthMax` (З-20), > интервал fetch 15 минут (З-21), грамматика «…с половиной» (З-26, > О-2-Б), `Progression.IsoStepKg` (З-27, О-3-А). Ревью №3: решение > ОВ-11 — импорт пакета программы (З-31, О-4-Б). ## 1. Решения открытых вопросов (закрытие раздела 7 requirements) | # ОВ | Решение | Обоснование | |---|---|---| | 1. Максимальная ЧСС | Формула Танаки: `HRmax = 208 − 0,7 × возраст`; ручной ввод `Hr.MaxBpm` перекрывает расчёт | Точнее для 45+ (профиль пользователя); ручной ввод — окончательная истина | | 2. Офлайн-STT | **Vosk** (малая русская модель ~50 МБ) + узкая грамматика команд (Ф-1.2: числа и слова «подход/вес/отдых/секунд/РПЭ» — «РПЭ 8» ставит RPE последнему подходу, Ф-13.3); TTS-подтверждения — системный Android TTS | Работа без сети, малый словарь → устойчивость к шуму; sherpa-onnx — запасная кандидатура, если точность < КУ-1 на контрольной тренировке | | 3. Канал телефон ↔ часы | Connect IQ **Communications SDK** (companion-канал через приложение Garmin Connect Mobile на телефоне). Передаётся: текущий пульс (≈1 Гц), зона, таймер отдыха (секунды до конца), заряд часов. Ограничение: на телефоне должно быть установлено и залогинено Garmin Connect Mobile; канал может не доставлять — обе стороны деградируют молча (Ф-2.4, Ф-3.2) | Единственный штатный канал сторонних приложений Garmin; проверка объёма передачи — прототипом в итерации 1 шага 7 (критерий: пульс и таймер доходят при экране часов активным) | | 4. Расписание fetch | Внешний планировщик ОС: systemd timer (Linux-сервер) / Планировщик задач (Windows) вызывает `--mode fetch`; внутри сервера планировщика нет. Интервал вызова — 15 минут (параметр планировщика ОС, в код не входит); вместе с лагом синхронизации часов с Garmin Connect укладывается в БЦ-5 (архив ≤ 30 минут после тренировки) | Проще, надёжнее, не дублирует средства ОС | | 10. Источник .fit атлетов без учётки владельца | v1 — только папка импорта (`--mode import`); `fetch` работает под единственной учёткой владельца; собственные учётки атлетов в конфиге — кандидат v2. Уточнение владельца: атлет регистрируется на телефоне (имя), привязка — по uuid клиентского приложения | Не раздувать v1; деградация уже работает (решение владельца О-1-А, З-19) | | 11. Импорт пакета программы | `--mode import` принимает `program-import.json` (формат — `program-import-format.md`): пакет маппится на `src.program` (`external_id` UNIQUE) и `src.program_version.body`; повторный импорт с тем же `external_id` — новая версия `program_version`; секция `profile` пакета в v1 не переносится — Safety-порогами управляет только конфиг `Safety.*` | Пакет — механизм эксплуатации (уточнение владельца); иначе путь доставки программы атлету в v1 не закрыт (ревью №3, О-4-Б) | | 5. Формула 1ПМ | **Эпли**: `1ПМ = w × (1 + reps/30)` | Стандарт индустрии; при reps ≤ 10 различие с Бжицки < 2 % | | 6. UI телефона | **Jetpack Compose** + Material 3 (тёмная тема по умолчанию) | Актуальный стек Kotlin; крупные зоны касания — Min touch target 48 dp (Ф-3.6) | | 7. Модели часов v1 | Таргет v1 — `fenix6x` (280×280, MIP, кнопки). Fenix 6 (260×260) и 6S (240×240) добавляются отдельными таргетами с собственными макетами (Ф-2.8) по фактическому парку (уточнение владельца 2026-10-17, изменение vision 1.5) | Якорные часы владельца — Fenix 6X; «одно разрешение 260×260» в ТЗ — описка исходника | | 8. Сопоставление телефон ↔ .fit | Окно ± 15 минут по времени начала (Ф-8.4); при нескольких кандидатах — ближайший по времени | ✅ закрыт в requirements | ## 2. Форматы обмена ### 2.1. Конверт синхронизации (телефон → сервер) `POST /api/v1/sync`, JSON-массив записей (пакет ≤ 50), каждая: ```json { "client_guid": "uuid-v4", "entity": "workout | workout_set | exercise | program_version | profile | daily_log | break", "op": "upsert", "data": { ...схема сущности... } } ``` Ответ: `{ "accepted": ["guid1", ...] }` (все или ничего — пакет атомарен; 409 при конфликте версии программы — перезапись на телефоне после перечитывания). Повтор пакета безопасен: сервер отвечает тем же `accepted` по уже принятым GUID (идемпотентность, Ф-7.4). Р2-Б: PK клиентских сущностей — UUID (генерирует телефон; строки «только .fit» — сервер): ссылки дочерних записей — те же UUID, маппинг id не нужен. ### 2.2. Сущности `data` (ключевые поля) - `exercise`: `exercise_id` (uuid), `name`, `group` (push|pull|legs|core|…), `is_base` (bool), `is_active` (bool) — повторная отправка с `is_active=false` — логическое удаление на сервере (З-41(а)); - `workout`: `workout_id` (uuid), `started_at`/`ended_at` (UTC ISO 8601), `status` (active|finished|cancelled), `session_kind` (normal|return|deload|test — Ф-14.5), `program_version_id` (nullable), `avg_hr`; - `workout_set`: `workout_id`, `exercise_id`, `seq`, `weight_kg`, `reps`, `done_at`, `source` (voice|watch|edit), `in_plan` (bool), `rpe` (int? 1–10, З-7), `knee_pain` (int? 0–10, только группа «ноги»); - `profile`: `sex`, `birth_year`, `weight_kg`, `hr_max` (nullable); - `break`: `kind` (NULL до ответа атлета), `source` (телефон передаёт `manual` — объявление простоя заранее; `auto` проставляет сервер по пропускам ≥ 4 дней, Ф-14.1), `started_on`, `planned_return`, `note`; `ended_on` проставляет сервер первой сессией после возврата (совпадает с `src.breaks`, З-17); - `daily_log`: `date` (YYYY-MM-DD), `bp_sys`, `bp_dias`, `weight_kg`, `hr_resting`, `wellbeing` (1–5 — Р1-А, поля v1; v2 добавит `waist_cm`, `sleep_h`); - `program_version`: JSON-схема программы из дизайна LLM-планирования (разд. 2) + `athlete_id` (выводится из ключа, в теле не передаётся). ### 2.3. Канонический JSON тренировки (сервер, из .fit) Из `FitParser` → `workout_facts.facts_json`: `session` (тип, старт, длительность), `laps[]` (индекс, старт, длительность), `records[]` (`t_offset_s`, `hr`), `sets[]` (упражнение/повторы/вес — если писались), `fit_file_id`, `device`. Схема версионная: `"schema": "workout-facts/1"`. ### 2.4. CSV-выгрузка (AdminWeb) Разделитель — `;`, кодировка UTF-8 c BOM (Excel), заголовок первой строкой: `athlete;date;duration_min;avg_hr;sets;exercises;adherence_pct`. Строка — тренировка; выборка = текущий фильтр таблицы. ## 3. Спецификация потребления Kit.Helpers Библиотека — `3-app\1-lib\Kit.Helpers` (std1, net10.0, Npgsql 10); потребление без изменений исходников (полная спецификация — [kit-helpers-spec.md](kit-helpers-spec.md)): - `Kit.Helpers.SqlHelperAbstract` (`SqlHelperPostgres`, fluent `SqlHelperFluent`) — доступ к PostgreSQL (единственный путь к БД из кода сервера; строка подключения — `Db.ConnectionString`; З-42 — фактическое имя компонента); - `Log.Logger` — файловые логи сервера (`Log.Path`, уровень `Info`); в sync-контуре v1 используется `ILogger` (Kit `Log.Logger` не настроен и без папки логов бросает — errata m04 №4; файловые логи — последующие модули); - `JsonHelper` — в v1 не потребляется (System.Text.Json напрямую, З-42); - остальное (Cache, Http, Session) в v1 не потребляется. ## 4. Схема конфигурации и правила валидации ### 4.1. Телефон (Preferences) | Параметр | Тип | Валидация | |---|---|---| | `Server.BaseUrl` | string | абсолютный https:// URL — иначе экран настроек не даёт сохранить | | `Server.ApiKey` | string | непустой | | `Sync.WifiOnly` | bool | — | | `Hr.MaxBpm` | int? | 120–240 или пусто (расчёт по Танаке) | | `Rest.DefaultSec` | int | 30–600 | | `Safety.BpCancelSys / BpCancelDias` | int | умолчания 150/90 (документ метрик, Р3-А) | | `Safety.BpCardioSys / BpCardioDias` | int | умолчания 140/90; ≤ BpCancel | | `Safety.RestingHrSitting` | int | умолчание 90 | | `Safety.HrStrengthMax` | int | 100–180 (умолчание 120, З-20) | | `Safety.LightWeekHrRise` | int | умолчание 8 | 🛡 Р3-А: телефонные `Safety.*` — источник экрана допуска (офлайн); серверные `Safety.*` (п. 4.2) — для серверных правил (`red_flags`); совпадение значений — ответственность владельца конфигурации (общая синхронизируемая сущность — кандидат v2). ### 4.2. Сервер (`appsettings.json`) | Параметр | Тип | Валидация (startup, `--mode *`) | |---|---|---| | `Api.Key` | string | непустой; уникальность среди `Api.Athletes[].apiKey` | | `Api.Athletes` | list | `{name}` непусто, `{apiKey}` непустой; дубликаты ключей — фатально | | `Admin.Login / Admin.PasswordHash` | string | непустые; хэш — PBKDF2 (100k итераций), префикс `pbkdf2$` | | `Db.ConnectionString` | string | тест подключения при старте — фатально при неудаче | | `Garmin.Login/Password` | string | обязательны для `--mode fetch` (иначе код возврата 1) | | `Garmin.AthleteName` | string | обязателен для `--mode fetch` (m07, решение гейта 1.3): имя атлета `src.athlete`, на которого пишутся факты (изоляция 🛡); не найден → код 1 | | `Garmin.DownloadDir` | path | умолчание `data it`; папка-кэш скачанных .fit (по activityId) | | `Garmin.BaseUrl` | string | умолчание `https://connect.garmin.com`; замена точек входа для тестов/прокси | | `Garmin.DownloadDir / Import.Dir / Import.ArchiveDir` | path | создание при старте; недоступность — фатально | | `Progression.BaseStepKg` | double | 0,5–10 | | `Progression.IsoStepKg` | double | 0,25–5 (умолчание 1,0 — коридор метрик «+0,5–1 кг», решение владельца О-3-А, З-27) | | `Progression.BackoffPercent` | int | 1–20 | | `Deload.MaxWeeks` | int | 3–12 | | `Safety.BpCancelSys / BpCancelDias` | int | 120–200 / 70–130 (умолчания 150/90) | | `Safety.BpElevatedSys / BpElevatedDias` | int | умолчания 140/95 (З-37: переименовано из `BpCardio*`, Dias 95 по факту кода; проверка «≤ BpCancel» не применяется — коридоры АД документа метрик перекрываются, spec 01 m04) | | `Safety.HrRestingDays` | int | 1–30 (умолчание 14) — окно базового пульса покоя | | `Safety.HrBaselineMin` | int | 1–14 (умолчание 3) — минимум значений для базового | | `Safety.WellbeingDays` | int | 1–7 (умолчание 2) — длина серии самочувствия | | `Safety.LightDayCount` | int | 2–7 (умолчание 3) — порог отмены/лимит серии | | `Safety.KneePainMin` | int | 1–5 (умолчание 1) | | `Safety.KneeRepeatSessions` | int | 2–5 (умолчание 2) | | `Progression.MissDaysForBreak` | int | 2–14 (умолчание 4, Ф-14.1) | | `Progression.ReturnMinFactor / ReturnMaxFactor` | double | 0.3–0.9 / (0.5; 1] (умолчания 0.6/0.9) — границы коэффициента возврата | | `Db.MigrationsDir` | path | каталог миграций `--mode migrate` (З-36); умолчание — migrations рядом с исполняемым файлом | | `Log.Path / Log.Level` | — | уровень: Debug/Info/Error | Примечания (З-37 п. 3): `Safety.RestingHrSitting`, `Safety.HrStrengthMax`, `Safety.LightWeekHrRise` (разд. 4.1) — потребление — телефон (m09); сервер в v1 не читает (мёртвых ключей в appsettings нет, Р3-А). Регистр ключей — camelCase по факту конфигов (З-37 п. 5). Валидация конфигурации выполняется при старте любого режима; ошибки — в лог и stderr, код возврата 1 (раздел 2.3 requirements). ## 5. Технические решения по компонентам - **`FitFetcher`**: реализация логина Garmin Connect (SSO-последовательность) на C# — по контракту семьи библиотек `garminconnect` (подписи запросов, OAuth1-заголовок); при блокировке — решение ОВ шага 7 (fallback: папка импорта уже работает независимо). Пометка прототипа итерации 1. - **`FitParser`**: официальный **Garmin FIT SDK for C#**; парсинг — потоковый, файл читается один раз; сбой одного файла не останавливает пакет (код возврата 2). - **`SyncQueue`**: таблица Room `sync_pending(guid, entity, payload, created_at)`; отправка — WorkManager (сетевое ограничение учитывает `Sync.WifiOnly`); ретрай — экспоненциально, 1 мин → 64 мин. - **`VoiceInput`**: Vosk-API (Kaldi) как service в процессе приложения; грамматика — JSON-словарь `[«подход», «вес», «отдых», «на», «секунд», «с половиной», 0–999]`; числа — постобработка («сто двадцать» → 120; «…с половиной» → +0,5 кг — решение владельца О-2-Б, З-26). - **`AdminWeb`** (решение владельца 2026-10-17): отдельное Flutter Web-приложение (`3-app\3-web\admin-web`); бэкенд — admin REST API в `Server.WatchBody.Web` (`/api/admin/*`): логин (логин/пароль → Bearer-токен сессии), атлеты (список/создание/смена ключа/ блокировка), тренировки, программы, метрики, CSV-выгрузка (генерация CSV — на сервере, п. 2.4). Пароль — PBKDF2 (см. 4.2); графики — штатные средства Flutter. - **HTTPS**: обратный прокси (nginx/Caddy) терминирует TLS; Kestrel — только localhost — вопрос развёртывания, вне кода. ## 6. UI/UX-стандарт (обязательные правила) Источник — [ui-ux-recommendations.md](ui-ux-recommendations.md); проверяемые требования: 1. **Часы:** семантика кнопок неизменна на всех экранах (UP/DOWN — выбор, START — подтвердить, BACK — отмена); ключевое действие — ≤ 3 нажатий и ≤ 5 секунд; шрифт ключевых цифр ≥ 22–26 px (`FONT_NUMBER_*`); предупреждения Safety закрываются только явной кнопкой (не исчезают сами); вибро-паттерны различаются (подход/отдых/предупреждение); планирование/графики/настройки на часах запрещены. 2. **Телефон:** шрифт ≥ 16 pt, контраст WCAG AA (≥ 4.5:1), цели касания ≥ 44 pt (требования Ф-3.6); действия одной руки — в нижней трети экрана; пикеры с шагом 2,5 кг, умолчание — последний вес; RPE/боль — сегмент-контролы; уведомления — только значимые; настройка размера шрифта. 3. **Админ-консоль:** десктоп-first; подтверждение предложений — дифф + «Принять/Отклонить/Править»; экспорт/бэкап одной кнопкой. 4. **Приёмка:** эвристическое ревью по 11 эвристикам (MDPI 2024) на каждый экран; SUS ≥ 80 — порог релиза; приёмка часов — только на реальном Fenix (симулятор — для разработки). ## 7. Чек-лист шага 7 (проверки прототипами) 1. Companion-канал: пульс/таймер доходят на телефон (ОВ-3) — итерация 1. 2. Логин Garmin Connect из C# (FitFetcher) — итерация 2. 3. Vosk: точность команд в шумном зале ≥ КУ-1 — итерация 2. 4. Сопоставление телефон ↔ .fit на реальной тренировке — итерация 3.