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

154 lines
12 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.4,
> [requirements.md](../1-reqs/requirements.md) (утверждён, раздел 7 —
> открытые вопросы), [architecture.md](../2-arch/architecture.md)
> (утверждена), дизайн LLM-планирования, документ форматов.
> Версия 1.1 — **УТВЕРЖДЕНО** (2026-10-17; интеграция документа метрик —
> Ф-13, Safety-пороги, daily_log — утверждена вместе с документом).
> Версия 1.2 — черновик (2026-10-17): errata ревью (З-6, З-11) +
> интеграция документа перерывов (Ф-14: сущность `break`,
> `session_kind`). Утверждение — Пользователь!
## 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`; внутри сервера планировщика нет | Проще, надёжнее, не дублирует средства ОС |
| 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 | Таргет `fenix6` (Fenix 6/6S/6X, включая Pro/Sapphire — одно разрешение 260×260); добавление моделей — новыми таргетами по фактическому парку (шаг 7) | Якорное устройство ТЗ; 6S/6X отличаются размером корпуса, не экраном |
| 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.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? 0–10),
`knee_pain` (int? 0–10, только группа «ноги»);
- `profile`: `sex`, `birth_year`, `weight_kg`, `hr_max` (nullable);
- `break`: `kind` (NULL до ответа атлета), `started_on`, `note`,
`planned_return` — break ставится автоматически (детекция по
пропускам ≥ 4 дней), причина запрашивается у атлета (Ф-14.1–2);
- `daily_log`: `date` (YYYY-MM-DD), `bp_sys`, `bp_dias`, `weight_kg`
(Ф-13.1; v2 добавит `hr_resting`, `waist_cm`, `sleep_h`, `wellbeing`);
- `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 |
### 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.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.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).
- **`AdminWeb`**: Razor Pages, сессия по cookie после логина;
пароль — PBKDF2 (см. 4.2); графики — Chart.js (локальный ассет,
без CDN).
- **HTTPS**: обратный прокси (nginx/Caddy) терминирует TLS; Kestrel —
только localhost — вопрос развёртывания, вне кода.
## 6. Чек-лист шага 7 (проверки прототипами)
1. Companion-канал: пульс/таймер доходят на телефон (ОВ-3) — итерация 1.
2. Логин Garmin Connect из C# (FitFetcher) — итерация 2.
3. Vosk: точность команд в шумном зале ≥ КУ-1 — итерация 2.
4. Сопоставление телефон ↔ .fit на реальной тренировке — итерация 3.