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

207 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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-А).
## 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) |
| 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);
- `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)):
- `Db.Postgres.SqlServerHelper` — доступ к PostgreSQL (единственный
путь к БД из кода сервера; строка подключения — `Db.ConnectionString`);
- `Log.Logger` — файловые логи сервера (`Log.Path`, уровень `Info`);
- `JsonHelper` — сериализация фактов/программ;
- остальное (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.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.BpCardioSys / BpCardioDias` | int | ≤ BpCancel (умолчания 140/90) |
| `Safety.RestingHrSitting` | int | 60–120 (умолчание 90) |
| `Safety.HrStrengthMax` | int | 100–180 (умолчание 120, З-20) |
| `Safety.LightWeekHrRise` | int | 3–15 (умолчание 8) |
| `Log.Path / Log.Level` | — | уровень: Debug/Info/Error |
Валидация конфигурации выполняется при старте любого режима; ошибки —
в лог и 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`**: Razor Pages, сессия по cookie после логина;
пароль — PBKDF2 (см. 4.2); графики — Chart.js (локальный ассет,
без CDN).
- **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.