21 KiB
Технические требования: 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 — таргетfenix6x280×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), каждая:
{
"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, fluentSqlHelperFluent) — доступ к PostgreSQL (единственный путь к БД из кода сервера; строка подключения —Db.ConnectionString; З-42 — фактическое имя компонента);Log.Logger— файловые логи сервера (Log.Path, уровеньInfo); в sync-контуре v1 используетсяILogger<SyncService>(KitLog.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.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: таблица Roomsync_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; проверяемые требования:
- Часы: семантика кнопок неизменна на всех экранах (UP/DOWN —
выбор, START — подтвердить, BACK — отмена); ключевое действие —
≤ 3 нажатий и ≤ 5 секунд; шрифт ключевых цифр ≥ 22–26 px
(
FONT_NUMBER_*); предупреждения Safety закрываются только явной кнопкой (не исчезают сами); вибро-паттерны различаются (подход/отдых/предупреждение); планирование/графики/настройки на часах запрещены. - Телефон: шрифт ≥ 16 pt, контраст WCAG AA (≥ 4.5:1), цели касания ≥ 44 pt (требования Ф-3.6); действия одной руки — в нижней трети экрана; пикеры с шагом 2,5 кг, умолчание — последний вес; RPE/боль — сегмент-контролы; уведомления — только значимые; настройка размера шрифта.
- Админ-консоль: десктоп-first; подтверждение предложений — дифф + «Принять/Отклонить/Править»; экспорт/бэкап одной кнопкой.
- Приёмка: эвристическое ревью по 11 эвристикам (MDPI 2024) на каждый экран; SUS ≥ 80 — порог релиза; приёмка часов — только на реальном Fenix (симулятор — для разработки).
7. Чек-лист шага 7 (проверки прототипами)
- Companion-канал: пульс/таймер доходят на телефон (ОВ-3) — итерация 1.
- Логин Garmin Connect из C# (FitFetcher) — итерация 2.
- Vosk: точность команд в шумном зале ≥ КУ-1 — итерация 2.
- Сопоставление телефон ↔ .fit на реальной тренировке — итерация 3.