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

233 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Технические требования: 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. Канал телефон ↔ часы | **Штатная трансляция пульса часов «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), каждая:
```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<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](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.