VEU.WathBody/1-docs/3-tech/tech-requirements.md

22 KiB
Raw Permalink Blame History

Технические требования: WatchBody («Феникс 6X») v1

Шаг 5 процесса разработки (std1\_process\process.md). Источники: vision.md v1.5, requirements.md (утверждён, раздел 7 — открытые вопросы), architecture.md (утверждена), дизайн LLM-планирования, документ форматов, UI/UX-рекомендации (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. Канал телефон ↔ часы Штатная трансляция пульса часов «Broadcast Heart Rate», приём по BLE — стандартный Heart Rate Service 0x180D (errata ОВ-3 от 2026-10-19: companion-канал Connect IQ на fenix 6X нереализуем — CIQ ≤ 4.1.7, Communications в полях данных только с API 5.0). Передаётся: текущий пульс (≈1 Гц), заряд часов (Battery Service, если транслируется). Канал односторонний (часы → телефон): таймер отдыха на часах автономный (настройка поля, умолчание Rest.DefaultSec; Ф-2.4 errata). Транспорт скрыт интерфейсом WatchTransport (мультичасовая инфраструктура, указание владельца 2026-10-19: любые часы со стандартной BLE-трансляцией работают без изменений кода; CIQ 5+/ANT+ — будущие реализации интерфейса). Garmin Connect Mobile не требуется; канал может не доставлять — обе стороны деградируют молча (Ф-2.4, Ф-3.2) Проверка на устройстве владельца в m12: пульс доходит на телефон при активном экране часов
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), каждая:

{
  "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.SqlHelperAbstract (SqlHelperPostgres, fluent SqlHelperFluent) — доступ к PostgreSQL (единственный путь к БД из кода сервера; строка подключения — Db.ConnectionString; З-42 — фактическое имя компонента);
  • Log.Logger — файловые логи сервера (Log.Path, уровень Info); в sync-контуре v1 используется ILogger<SyncService> (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; проверяемые требования:

  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. Канал часов: пульс доходит на телефон при активном экране часов (ОВ-3; errata 2026-10-19 — транспорт BLE-трансляция «Broadcast Heart Rate», таймер отдыха автономный на часах — m13; вибрация конца отдыха и метрики подхода Lap→Lap — приёмка m13) — итерация 5 (m12).
  2. Логин Garmin Connect из C# (FitFetcher) — итерация 2.
  3. Vosk: точность команд в шумном зале ≥ КУ-1 — итерация 2.
  4. Сопоставление телефон ↔ .fit на реальной тренировке — итерация 3.