Волна 2 ревью по решениям владельца: Р1-А (пульс покоя/самочувствие в v1), Р2-Б (UUID PK клиентских сущностей), Р3-А (Safety на телефоне); З-7/З-8; UI/UX-стандарт в техтребованиях

This commit is contained in:
virus-home 2026-10-17 19:54:31 +05:00
parent 1034c152f5
commit 36eb1741dd
7 changed files with 230 additions and 50 deletions

View File

@ -179,7 +179,7 @@ architecture → 1.1, tech-requirements → 1.3, tables/functions → новая
|---|---|---|---| |---|---|---|---|
| Р1–Р3 решения владельца | — | — | ⬜ | | Р1–Р3 решения владельца | — | — | ⬜ |
| З-1 правило 1: данные v1 | 1 | Р1 | ⬜ | | З-1 правило 1: данные v1 | 1 | Р1 | ⬜ |
| З-2 архитектура + hld.puml + рендер | 1 | — (текст), рендер после правок | 🔄 (2026-10-17: Ф-13+Ф-14 внесены; черновик v1.1 — на утверждении) | | З-2 архитектура + hld.puml + рендер | 1 | — (текст), рендер после правок | 🔄 (2026-10-17: Ф-13, Ф-14 и волна 2 внесены; черновик v1.1 — на утверждении) |
| З-3 client_guid + маппинг id | 1 | Р2 | ⬜ | | З-3 client_guid + маппинг id | 1 | Р2 | ⬜ |
| З-4 Safety-пороги телефона | 1 | Р3 | ⬜ | | З-4 Safety-пороги телефона | 1 | Р3 | ⬜ |
| З-5 fit_file_id / fit_workout_id | 2 | — | ✅ (2026-10-17) | | З-5 fit_file_id / fit_workout_id | 2 | — | ✅ (2026-10-17) |

View File

@ -11,6 +11,15 @@
> пользователя): интегрирован документ > пользователя): интегрирован документ
> `Метрики_тренировок_и_самоконтроля.md` — добавлена Ф-13, правила > `Метрики_тренировок_и_самоконтроля.md` — добавлена Ф-13, правила
> Ф-12 расширены (vision 1.3, БЦ-6, КУ-7). > Ф-12 расширены (vision 1.3, БЦ-6, КУ-7).
> Версия 1.2 — черновик (2026-10-17): errata ревью
> (`0-mng\review-2026-10-17.md`, З-9–З-11) + интеграция документа
> `Перерывы_подводные_камни_и_рекомендации.md` — Ф-14 (изменение
> vision 1.4).
> Версия 1.3 — черновик (2026-10-17): волна 2 ревью по решениям
> владельца — Р1-А (пульс покоя/самочувствие в v1), Р2-Б (UUID как PK
> клиентских сущностей), Р3-А (Safety-пороги на телефоне); З-7 (RPE
> 1–10), З-8 (команда «РПЭ N»); интеграция UI/UX-рекомендаций
> (`3-tech\ui-ux-recommendations.md`). Утверждение — Пользователь!
## 1. Назначение и границы ## 1. Назначение и границы
@ -94,6 +103,10 @@ Garmin штатно — Ф-2.5, Ф-2.6). Код поля данных перен
| `Voice.Engine` | офлайн-движок распознавания (фиксируется шагом 5, открытый вопрос 2) | шаг 5 | | `Voice.Engine` | офлайн-движок распознавания (фиксируется шагом 5, открытый вопрос 2) | шаг 5 |
| `Voice.Language` | язык команд | `ru` | | `Voice.Language` | язык команд | `ru` |
| `Rest.DefaultSec` | отдых по умолчанию, если не задан командой | `120` | | `Rest.DefaultSec` | отдых по умолчанию, если не задан командой | `120` |
| `Safety.BpCancel` | порог отмены тренировки по АД (сист./диаст.) | `150/90` |
| `Safety.BpCardio` | порог «только кардио» по АД | `140/90` |
| `Safety.RestingHrSitting` | порог пульса сидя для переноса тренировки | `90` |
| `Safety.LightWeekHrRise` | рост пульса покоя (2 дня) для лёгкой недели | `+8` |
**Сервер (`appsettings.json`):** **Сервер (`appsettings.json`):**
@ -132,7 +145,8 @@ Garmin штатно — Ф-2.5, Ф-2.6). Код поля данных перен
- «Подход **W** на **R**» → подход: вес `W` кг × `R` повторений - «Подход **W** на **R**» → подход: вес `W` кг × `R` повторений
(целые числа; `W` — 0–500, `R` — 1–100); (целые числа; `W` — 0–500, `R` — 1–100);
- «Вес **W**» → рабочий вес `W` кг для следующих подходов; - «Вес **W**» → рабочий вес `W` кг для следующих подходов;
- «Отдых **S** (секунд)» → таймер отдыха `S` секунд (5–600). - «Отдых **S** (секунд)» → таймер отдыха `S` секунд (5–600);
- «РПЭ **N**» → RPE последнего подхода, N 1–10 (Ф-13.3, З-8).
3. Команда вне грамматики или с числом вне диапазона — отклоняется: 3. Команда вне грамматики или с числом вне диапазона — отклоняется:
короткий сигнал ошибки, состояние не меняется, в лог пишется короткий сигнал ошибки, состояние не меняется, в лог пишется
распознанный текст. распознанный текст.
@ -190,7 +204,12 @@ Garmin штатно — Ф-2.5, Ф-2.6). Код поля данных перен
тренировки с подтверждением — тренировка помечается `cancelled` тренировки с подтверждением — тренировка помечается `cancelled`
и сохраняется в историю). и сохраняется в историю).
6. На экране одновременно ≤ 4 активных элемента, тёмная тема, 6. На экране одновременно ≤ 4 активных элемента, тёмная тема,
крупные зоны касания (vision 2.2). крупные зоны касания (vision 2.2); UI/UX-стандарт
(`3-tech\ui-ux-recommendations.md`): шрифт ≥ 16 pt, контраст WCAG AA,
цели касания ≥ 44 pt, действия одной руки — в нижней трети экрана;
числовой ввод — пикеры с шагом 2,5 кг (не клавиатура), значение по
умолчанию — последний использованный вес; цветовые сигналы
дублируются текстом/иконкой (не только цветом).
### Ф-4. Библиотека упражнений (телефон, синхронизируется на сервер) ### Ф-4. Библиотека упражнений (телефон, синхронизируется на сервер)
@ -233,6 +252,10 @@ Garmin штатно — Ф-2.5, Ф-2.6). Код поля данных перен
порядок — по времени создания; отправка — пакетами ≤ 50 записей. порядок — по времени создания; отправка — пакетами ≤ 50 записей.
🛡 Атлета определяет API-ключ: все записи пакета пишутся на сервере 🛡 Атлета определяет API-ключ: все записи пакета пишутся на сервере
от его имени; телефон не передаёт идентификатор атлета в данных. от его имени; телефон не передаёт идентификатор атлета в данных.
🛡 Каждая клиентская сущность идентифицируется UUID — первичным
ключом и на телефоне, и на сервере (Р2-Б); UUID генерирует телефон
(строки «только .fit» — сервер); ссылки дочерних записей — по тем же
UUID, отдельный маппинг серверных id не нужен.
3. Отправка: `POST /api/v1/sync` (пакет записей + `client_guid` каждой); 3. Отправка: `POST /api/v1/sync` (пакет записей + `client_guid` каждой);
успех записи на сервере подтверждается ответом (принятые GUID); успех записи на сервере подтверждается ответом (принятые GUID);
подтверждённые удаляются из очереди. Неудача сети — тихий баннер подтверждённые удаляются из очереди. Неудача сети — тихий баннер
@ -371,7 +394,9 @@ Garmin штатно — Ф-2.5, Ф-2.6). Код поля данных перен
4. Делоад: если `Deload.MaxWeeks` недель подряд без делоада, правило 4. Делоад: если `Deload.MaxWeeks` недель подряд без делоада, правило
предлагает делоад-версию (объём −40–50 %) — тем же механизмом предлагает делоад-версию (объём −40–50 %) — тем же механизмом
диффа (дизайн LLM-планирования, разд. 6). диффа (дизайн LLM-планирования, разд. 6).
5. Красные флаги и лёгкая неделя (документ метрик, правила 1, 6, v1): 5. Красные флаги и лёгкая неделя (документ метрик, правила 1, 6, v1);
источник данных — `daily_log` (тренд пульса покоя и самочувствия
за 2 дня, Р1-А):
- пульс покоя ≥ обычного +8 уд/мин два дня подряд или самочувствие - пульс покоя ≥ обычного +8 уд/мин два дня подряд или самочувствие
≤ 2 → следующая тренировка помечается лёгкой (60–70 % весов, ≤ 2 → следующая тренировка помечается лёгкой (60–70 % весов,
2 сета); 3+ дня — предложение отмены и совет к врачу; 2 сета); 3+ дня — предложение отмены и совет к врачу;
@ -390,8 +415,10 @@ Garmin штатно — Ф-2.5, Ф-2.6). Код поля данных перен
(его п. 5). (его п. 5).
1. **Дневник-минимум (день, дом, 2–3 минуты):** АД (сист./диаст.), 1. **Дневник-минимум (день, дом, 2–3 минуты):** АД (сист./диаст.),
вес тела; экран «Сегодня» с вводом за 2–3 касания. Данные — вес тела, пульс покоя (утро, до подъёма — Р1-А), самочувствие 1–5;
в `LocalDb` и на сервер (`daily_log`), в сводку метрик атлета. экран «Сегодня» с вводом за 2–3 касания. Данные — в `LocalDb`
и на сервер (`daily_log`); пульс покоя и самочувствие — данные
правила 1 (Ф-12.5, КУ-7).
2. **Предстартовый допуск:** перед стартом силовой тренировки — экран 2. **Предстартовый допуск:** перед стартом силовой тренировки — экран
допуска: пульс сидя (с часов, если связаны, иначе ввод вручную) допуска: пульс сидя (с часов, если связаны, иначе ввод вручную)
и АД за сегодня. Пороги: и АД за сегодня. Пороги:
@ -401,17 +428,17 @@ Garmin штатно — Ф-2.5, Ф-2.6). Код поля данных перен
- пульс сидя > 90 уд/мин → предложение перенести тренировку - пульс сидя > 90 уд/мин → предложение перенести тренировку
(старт — явным подтверждением «всё равно начать»). (старт — явным подтверждением «всё равно начать»).
3. **По сетам:** RPE (цель 6–8) — голосом «РПЭ **N**» для последнего 3. **По сетам:** RPE (цель 6–8) — голосом «РПЭ **N**» для последнего
подхода или тапом 0–10; боль в колене 0–10 — тапом, только для подхода или тапом 1–10; боль в колене 0–10 — тапом, только для
упражнений группы «ноги»; сохраняется с подходом (`workout_set`). упражнений группы «ноги»; сохраняется с подходом (`workout_set`).
4. **После тренировки:** время возврата пульса к < 90 уд/мин — считается 4. **После тренировки:** время возврата пульса к < 90 уд/мин — считается
автоматически из .fit (Record после последнего Lap); показывается автоматически из .fit (Record после последнего Lap); показывается
в карточке и попадает в метрики. в карточке и попадает в метрики.
5. 🛡 Пороги допуска — конфигурируемые на сервере (`Safety.*`), 5. 🛡 Пороги допуска — конфигурируемые в настройках телефона
значения по умолчанию — из документа метрик; обход блокировки (`Safety.*`, умолчания — документ метрик; Р3-А); серверные
«Отменить» логируется. `Safety.*` применяются серверными правилами (`red_flags`);
6. Полная утренняя телеметрия (пульс покоя, талия, сон, самочувствие) обход блокировки «Отменить» логируется.
и правила координации 7–10 — v2; контрольные тесты — v3 6. Талия и сон (утренняя телеметрия) и правила координации 7–10 — v2;
(документ метрик, п. 5). контрольные тесты — v3 (документ метрик, п. 5).
### Ф-14. Перерывы и возврат в режим (телефон + сервер; изменение 2) ### Ф-14. Перерывы и возврат в режим (телефон + сервер; изменение 2)

View File

@ -14,9 +14,10 @@
- **Изоляция атлетов (🛡):** каждая таблица данных несёт `athlete_id`; - **Изоляция атлетов (🛡):** каждая таблица данных несёт `athlete_id`;
все функции слоя `src` принимают `p_athlete_id` и выбирают/пишут все функции слоя `src` принимают `p_athlete_id` и выбирают/пишут
только в пределах атлета (requirements, Ф-8.5, инвариант раздела 5). только в пределах атлета (requirements, Ф-8.5, инвариант раздела 5).
- **Идемпотентность (🛡):** клиентские сущности несут `client_guid` - **Идемпотентность (🛡, Р2-Б):** PK клиентских сущностей — UUID;
(UNIQUE) — повторная вставка возвращает существующий id телефон генерирует UUID (строки «только .fit» — сервер), повторная
(`ON CONFLICT DO NOTHING` + `SELECT`). вставка с тем же UUID возвращает существующую запись; ссылки дочерних
записей — те же UUID, маппинг серверных id не нужен.
- **JSON:** версии программ и факты .fit — `jsonb` с проверкой схемы - **JSON:** версии программ и факты .fit — `jsonb` с проверкой схемы
на стороне приложения (не в БД). на стороне приложения (не в БД).

View File

@ -9,8 +9,10 @@
> **УТВЕРЖДЕНО** (2026-10-17) — список функций к реализации утверждён. > **УТВЕРЖДЕНО** (2026-10-17) — список функций к реализации утверждён.
> Новая редакция (2026-10-17): + `breaks` (break_insert/update/select), > Новая редакция (2026-10-17): + `breaks` (break_insert/update/select),
> `session_kind` в workout_insert/update — интеграция документа > `session_kind` в workout_insert/update — интеграция документа
> перерывов (Ф-14, изменение vision 1.4). Утверждение добавки — > перерывов (Ф-14, изменение vision 1.4).
> Пользователь! > Волна 2 (Р2-Б): insert клиентских сущностей принимает `p_id (uuid)`
> (PK — UUID); Р1-А: `daily_log_upsert` — `p_hr_resting`, `p_wellbeing`.
> Утверждение новой редакции — Пользователь!
## Список функций ## Список функций
@ -36,18 +38,18 @@
| program_version | program_version_update | PLAN | p_athlete_id, p_id, p_status | int — число строк | - | - | 🛡 меняется только статус; переходы draft→active→archived; активной становится ровно одна (сдвиг прежней в archived) | | program_version | program_version_update | PLAN | p_athlete_id, p_id, p_status | int — число строк | - | - | 🛡 меняется только статус; переходы draft→active→archived; активной становится ровно одна (сдвиг прежней в archived) |
| program_version | program_version_select | PLAN | p_athlete_id, p_program_id, p_status, p_author | TABLE — строки (без body) | - | - | body — отдельным get (крупный jsonb) | | program_version | program_version_select | PLAN | p_athlete_id, p_program_id, p_status, p_author | TABLE — строки (без body) | - | - | body — отдельным get (крупный jsonb) |
| program_version | program_version_get | PLAN | p_athlete_id, p_id | TABLE — одна строка с body | - | - | — | | program_version | program_version_get | PLAN | p_athlete_id, p_id | TABLE — одна строка с body | - | - | — |
| workout | workout_insert | PLAN | p_athlete_id, p_client_guid, p_program_version_id, p_started_at, p_session_kind | int — id | - | - | повтор `client_guid` возвращает существующий id (🛡 идемпотентность) | | workout | workout_insert | PLAN | p_athlete_id, p_id (uuid), p_program_version_id, p_started_at, p_session_kind | int — id | - | - | повтор `client_guid` возвращает существующий id (🛡 идемпотентность) |
| workout | workout_update | PLAN | p_athlete_id, p_id, p_ended_at, p_status, p_session_kind, p_avg_hr, p_facts_json, p_fit_file_id, p_hr_recovery_s | int — число строк | - | - | факты .fit дописывает FitParser; клиент — только ended/status/session_kind/avg_hr | | workout | workout_update | PLAN | p_athlete_id, p_id, p_ended_at, p_status, p_session_kind, p_avg_hr, p_facts_json, p_fit_file_id, p_hr_recovery_s | int — число строк | - | - | факты .fit дописывает FitParser; клиент — только ended/status/session_kind/avg_hr |
| workout | workout_select | PLAN | p_athlete_id, p_status, p_date_from, p_date_to | TABLE — карточки + подходы (агрегат jsonb) | - | - | мультикурсор/агрегат: карточка + sets (d-db-6) | | workout | workout_select | PLAN | p_athlete_id, p_status, p_date_from, p_date_to | TABLE — карточки + подходы (агрегат jsonb) | - | - | мультикурсор/агрегат: карточка + sets (d-db-6) |
| workout | workout_get | PLAN | p_athlete_id, p_id | TABLE — одна строка + sets + command_log | - | - | полная карточка (Ф-9.2) | | workout | workout_get | PLAN | p_athlete_id, p_id | TABLE — одна строка + sets + command_log | - | - | полная карточка (Ф-9.2) |
| workout_set | workout_set_insert | PLAN | p_athlete_id, p_client_guid, p_workout_id, p_exercise_id, p_seq, p_weight_kg, p_reps, p_done_at, p_source, p_in_plan, p_rpe, p_knee_pain | int — id | - | - | идемпотентность по client_guid | | workout_set | workout_set_insert | PLAN | p_athlete_id, p_id (uuid), p_workout_id, p_exercise_id, p_seq, p_weight_kg, p_reps, p_done_at, p_source, p_in_plan, p_rpe, p_knee_pain | int — id | - | - | идемпотентность по client_guid |
| workout_set | workout_set_get | PLAN | p_athlete_id, p_id | TABLE — одна строка | - | - | — | | workout_set | workout_set_get | PLAN | p_athlete_id, p_id | TABLE — одна строка | - | - | — |
| command_log | command_log_insert | PLAN | p_athlete_id, p_client_guid, p_workout_id, p_t_offset_s, p_recognized_text, p_semantics, p_accepted | int — id | - | - | 🛡 не редактируется (Ф-1.5) | | command_log | command_log_insert | PLAN | p_athlete_id, p_id (uuid), p_workout_id, p_t_offset_s, p_recognized_text, p_semantics, p_accepted | int — id | - | - | 🛡 не редактируется (Ф-1.5) |
| command_log | command_log_select | PLAN | p_athlete_id, p_workout_id | TABLE — строки | - | - | для сопоставления с .fit | | command_log | command_log_select | PLAN | p_athlete_id, p_workout_id | TABLE — строки | - | - | для сопоставления с .fit |
| daily_log | daily_log_upsert | PLAN | p_athlete_id, p_client_guid, p_date, p_bp_sys, p_bp_dias, p_weight_kg | int — id | - | - | `UNIQUE(athlete_id, date)` — upsert; v2-поля добавляются расширением | | daily_log | daily_log_upsert | PLAN | p_athlete_id, p_id (uuid), p_date, p_bp_sys, p_bp_dias, p_weight_kg, p_hr_resting, p_wellbeing | uuid — id | - | - | `UNIQUE(athlete_id, date)` — upsert; hr_resting/wellbeing — поля v1 (Р1-А) |
| daily_log | daily_log_select | PLAN | p_athlete_id, p_date_from, p_date_to | TABLE — строки | - | - | для сводки метрик и AdminWeb | | daily_log | daily_log_select | PLAN | p_athlete_id, p_date_from, p_date_to | TABLE — строки | - | - | для сводки метрик и AdminWeb |
| workout_metrics | workout_metrics_upsert | PLAN | p_athlete_id, p_workout_id, p_adherence, p_tonnage, p_tonnage_by_group, p_e1pm_by_exercise, p_hr_metrics, p_red_flags | int — id | - | - | 1:1, пересчёт MetricsEngine | | workout_metrics | workout_metrics_upsert | PLAN | p_athlete_id, p_workout_id, p_adherence, p_tonnage, p_tonnage_by_group, p_e1pm_by_exercise, p_hr_metrics, p_red_flags | int — id | - | - | 1:1, пересчёт MetricsEngine |
| workout_metrics | workout_metrics_select | PLAN | p_athlete_id, p_workout_id, p_date_from, p_date_to | TABLE — строки | - | - | графики AdminWeb (Ф-9.4) | | workout_metrics | workout_metrics_select | PLAN | p_athlete_id, p_workout_id, p_date_from, p_date_to | TABLE — строки | - | - | графики AdminWeb (Ф-9.4) |
| breaks | break_insert | PLAN | p_athlete_id, p_client_guid, p_kind, p_source, p_started_on, p_note | int — id | - | - | auto-ставит сервер по пропускам; manual — телефон | | breaks | break_insert | PLAN | p_athlete_id, p_id (uuid), p_kind, p_source, p_started_on, p_note | int — id | - | - | auto-ставит сервер по пропускам; manual — телефон |
| breaks | break_update | PLAN | p_athlete_id, p_id, p_ended_on | int — число строк | - | - | закрытие break первой сессией после возврата | | breaks | break_update | PLAN | p_athlete_id, p_id, p_ended_on | int — число строк | - | - | закрытие break первой сессией после возврата |
| breaks | break_select | PLAN | p_athlete_id, p_kind, p_date_from, p_date_to | TABLE — строки | - | - | журнал перерывов; CSV-выгрузка (документ перерывов разд. 4) | | breaks | break_select | PLAN | p_athlete_id, p_kind, p_date_from, p_date_to | TABLE — строки | - | - | журнал перерывов; CSV-выгрузка (документ перерывов разд. 4) |

View File

@ -14,13 +14,18 @@
| `src.exercise_replacement` | связь ММ | exercise_replacement_insert, exercise_replacement_delete | MM | типовой набор связи ММ — d-db-3; пара-замена для правила 6 (документ метрик); выборка — внутри `exercise_select` | | `src.exercise_replacement` | связь ММ | exercise_replacement_insert, exercise_replacement_delete | MM | типовой набор связи ММ — d-db-3; пара-замена для правила 6 (документ метрик); выборка — внутри `exercise_select` |
| `src.program` | сущность | program_insert, program_select, program_get | CUSTOM | контейнер версий; без update/delete — жизненный цикл ведут версии | | `src.program` | сущность | program_insert, program_select, program_get | CUSTOM | контейнер версий; без update/delete — жизненный цикл ведут версии |
| `src.program_version` | сущность | program_version_insert, program_version_update, program_version_select, program_version_get | CRUD | `body` jsonb (схема дизайна LLM-планирования разд. 2); 🛡 неизменяемость: `_update` меняет только `status` | | `src.program_version` | сущность | program_version_insert, program_version_update, program_version_select, program_version_get | CRUD | `body` jsonb (схема дизайна LLM-планирования разд. 2); 🛡 неизменяемость: `_update` меняет только `status` |
| `src.workout` | сущность | workout_insert, workout_update, workout_select, workout_get | CRUD | `client_guid` UNIQUE — идемпотентность; `facts_json` — факты .fit | | `src.workout` | сущность | workout_insert, workout_update, workout_select, workout_get | CRUD | PK UUID — идемпотентность (Р2-Б); `facts_json` — факты .fit |
| `src.workout_set` | сущность | workout_set_insert, workout_select (в составе), workout_set_get | CUSTOM | выборка — агрегатом в `workout_select` (подходы в карточке); `_delete`/`_update` нет: правка = новая пометка `source=edit` (история сохраняется) | | `src.workout_set` | сущность | workout_set_insert, workout_select (в составе), workout_set_get | CUSTOM | выборка — агрегатом в `workout_select` (подходы в карточке); `_delete`/`_update` нет: правка = новая пометка `source=edit` (история сохраняется) |
| `src.command_log` | сущность | command_log_insert, command_log_select | CUSTOM | журнал голосовых команд (Ф-1.5, 🛡 не редактируется); update/delete нет | | `src.command_log` | сущность | command_log_insert, command_log_select | CUSTOM | журнал голосовых команд (Ф-1.5, 🛡 не редактируется); update/delete нет |
| `src.daily_log` | сущность | daily_log_upsert, daily_log_select | CUSTOM | день уникален для атлета (`UNIQUE(athlete_id, date)`) — upsert | | `src.daily_log` | сущность | daily_log_upsert, daily_log_select | CUSTOM | день уникален для атлета (`UNIQUE(athlete_id, date)`) — upsert |
| `src.workout_metrics` | сущность | workout_metrics_upsert, workout_metrics_select | CUSTOM | 1:1 к `workout`, считает сервер (MetricsEngine) — телефон только читает; upsert (пересчёт) | | `src.workout_metrics` | сущность | workout_metrics_upsert, workout_metrics_select | CUSTOM | 1:1 к `workout`, считает сервер (MetricsEngine) — телефон только читает; upsert (пересчёт) |
| `src.breaks` | сущность | break_insert, break_update, break_select | CUSTOM | перерывы (Ф-14); закрытие = update (`ended_at`); delete нет — журнал | | `src.breaks` | сущность | break_insert, break_update, break_select | CUSTOM | перерывы (Ф-14); закрытие = update (`ended_at`); delete нет — журнал |
> Новая редакция (2026-10-17, волна 2 ревью, решения владельца Р1-А/Р2-Б):
> PK клиентских сущностей — UUID (Р2-Б); `hr_resting`/`wellbeing` —
> поля v1 (Р1-А); `src.breaks`, `session_kind` — Ф-14. Утверждение —
> Пользователь!
## Описания ## Описания
### `src.athlete` — атлет ### `src.athlete` — атлет
@ -49,7 +54,7 @@
| Поле | Тип | Описание | | Поле | Тип | Описание |
|---|---|---| |---|---|---|
| `id` | serial PK | 🛡 неизменяемый `exercise_id` (Ф-4.3) | | `id` | uuid PK DEFAULT gen_random_uuid() | 🛡 неизменяемый `exercise_id` (Ф-4.3); UUID генерирует телефон (Р2-Б) |
| `athlete_id` | int NOT NULL | → `src.athlete.id`; 🛡 библиотека принадлежит атлету (Ф-4.4) | | `athlete_id` | int NOT NULL | → `src.athlete.id`; 🛡 библиотека принадлежит атлету (Ф-4.4) |
| `name` | text NOT NULL | название; `UNIQUE(athlete_id, name)` | | `name` | text NOT NULL | название; `UNIQUE(athlete_id, name)` |
| `group_id` | int NOT NULL | → `ref.exercise_group.id` | | `group_id` | int NOT NULL | → `ref.exercise_group.id` |
@ -79,7 +84,7 @@
| Поле | Тип | Описание | | Поле | Тип | Описание |
|---|---|---| |---|---|---|
| `id` | serial PK | `program_version_id` для тренировок | | `id` | uuid PK DEFAULT gen_random_uuid() | `program_version_id` для тренировок; UUID — телефон (Р2-Б) |
| `program_id` | int NOT NULL | → `src.program.id` | | `program_id` | int NOT NULL | → `src.program.id` |
| `version` | int NOT NULL | `UNIQUE(program_id, version)`; 1, 2, 3… | | `version` | int NOT NULL | `UNIQUE(program_id, version)`; 1, 2, 3… |
| `based_on_version` | int NULL | предыдущая версия (NULL — первая) | | `based_on_version` | int NULL | предыдущая версия (NULL — первая) |
@ -93,9 +98,8 @@
| Поле | Тип | Описание | | Поле | Тип | Описание |
|---|---|---| |---|---|---|
| `id` | serial PK | — | | `id` | uuid PK | UUID — телефон (строки «только .fit» — сервер); идемпотентность (Ф-7.4, Р2-Б) |
| `athlete_id` | int NOT NULL | → `src.athlete.id` | | `athlete_id` | int NOT NULL | → `src.athlete.id` |
| `client_guid` | uuid NOT NULL UNIQUE | идемпотентность (Ф-7.4) |
| `program_version_id` | int NULL | → `src.program_version.id`; версия, активная на начало (Ф-10.3) | | `program_version_id` | int NULL | → `src.program_version.id`; версия, активная на начало (Ф-10.3) |
| `started_at` | timestamptz NOT NULL | начало (UTC) | | `started_at` | timestamptz NOT NULL | начало (UTC) |
| `ended_at` | timestamptz NULL | конец | | `ended_at` | timestamptz NULL | конец |
@ -108,15 +112,14 @@
`UNIQUE(athlete_id, fit_file_id)` — единственная уникальность `fit_file_id`; сопоставление телефон ↔ .fit — окно `UNIQUE(athlete_id, fit_file_id)` — единственная уникальность `fit_file_id`; сопоставление телефон ↔ .fit — окно
±15 минут по `started_at` (Ф-8.4): факты пишутся в ту же строку ±15 минут по `started_at` (Ф-8.4): факты пишутся в ту же строку
(`facts_json`), либо создается строка «только .fit» без `client_guid` (`facts_json`), либо создается строка «только .fit» строкой «только .fit» — тогда UUID `UUID генерирует сервер (Р2-Б).
пары — тогда `client_guid` генерирует сервер.
### `src.workout_set` — подход (телефон) ### `src.workout_set` — подход (телефон)
| Поле | Тип | Описание | | Поле | Тип | Описание |
|---|---|---| |---|---|---|
| `id` | serial PK | — | | `id` | uuid PK | UUID — телефон (Р2-Б) |
| `workout_id` | int NOT NULL | → `src.workout.id` | | `workout_id` | uuid NOT NULL | → `src.workout.id` |
| `athlete_id` | int NOT NULL | денормализация для изоляции выборок | | `athlete_id` | int NOT NULL | денормализация для изоляции выборок |
| `exercise_id` | int NULL | → `src.exercise.id`; NULL — упражнение не выбрано | | `exercise_id` | int NULL | → `src.exercise.id`; NULL — упражнение не выбрано |
| `seq` | int NOT NULL | порядковый номер подхода в тренировке | | `seq` | int NOT NULL | порядковый номер подхода в тренировке |
@ -127,55 +130,51 @@
| `in_plan` | boolean NOT NULL | «по плану» (Ф-11.2) | | `in_plan` | boolean NOT NULL | «по плану» (Ф-11.2) |
| `rpe` | int NULL CHECK (rpe BETWEEN 1 AND 10) | субъективная нагрузка (Ф-13.3) | | `rpe` | int NULL CHECK (rpe BETWEEN 1 AND 10) | субъективная нагрузка (Ф-13.3) |
| `knee_pain` | int NULL CHECK (0–10) | боль в колене (группа `legs`, Ф-13.3) | | `knee_pain` | int NULL CHECK (0–10) | боль в колене (группа `legs`, Ф-13.3) |
| `client_guid` | uuid NOT NULL UNIQUE | идемпотентность |
### `src.command_log` — журнал голосовых команд (Ф-1.5, 🛡) ### `src.command_log` — журнал голосовых команд (Ф-1.5, 🛡)
| Поле | Тип | Описание | | Поле | Тип | Описание |
|---|---|---| |---|---|---|
| `id` | serial PK | — | | `id` | uuid PK | UUID — телефон (Р2-Б) |
| `workout_id` | int NOT NULL | → `src.workout.id` | | `workout_id` | uuid NOT NULL | → `src.workout.id` |
| `t_offset_s` | int NOT NULL | секунды от начала тренировки (сопоставление с .fit) | | `t_offset_s` | int NOT NULL | секунды от начала тренировки (сопоставление с .fit) |
| `recognized_text` | text NOT NULL | распознанный текст (диагностика STT) | | `recognized_text` | text NOT NULL | распознанный текст (диагностика STT) |
| `semantics` | jsonb NOT NULL | `{cmd, weight, reps, rest_sec, rpe}` | | `semantics` | jsonb NOT NULL | `{cmd, weight, reps, rest_sec, rpe}` |
| `accepted` | boolean NOT NULL | принята грамматикой/диапазоном | | `accepted` | boolean NOT NULL | принята грамматикой/диапазоном |
| `client_guid` | uuid NOT NULL UNIQUE | идемпотентность |
### `src.daily_log` — дневник самоконтроля (Ф-13.1) ### `src.daily_log` — дневник самоконтроля (Ф-13.1)
| Поле | Тип | Описание | | Поле | Тип | Описание |
|---|---|---| |---|---|---|
| `id` | serial PK | — | | `id` | uuid PK | UUID — телефон (Р2-Б) |
| `athlete_id` | int NOT NULL | → `src.athlete.id`; `UNIQUE(athlete_id, date)` | | `athlete_id` | int NOT NULL | → `src.athlete.id`; `UNIQUE(athlete_id, date)` |
| `date` | date NOT NULL | день (локальная дата атлета) | | `date` | date NOT NULL | день (локальная дата атлета) |
| `bp_sys` | int NULL | систолическое АД | | `bp_sys` | int NULL | систолическое АД |
| `bp_dias` | int NULL | диастолическое АД | | `bp_dias` | int NULL | диастолическое АД |
| `weight_kg` | numeric(5,1) NULL | вес тела | | `weight_kg` | numeric(5,1) NULL | вес тела |
| `hr_resting` | int NULL | пульс покоя (v2) | | `hr_resting` | int NULL | пульс покоя, утро (Р1-А — поле v1, данные правила 1) |
| `waist_cm` | numeric(5,1) NULL | талия (v2) | | `waist_cm` | numeric(5,1) NULL | талия (v2) |
| `sleep_h` | numeric(3,1) NULL | сон, часов (v2) | | `sleep_h` | numeric(3,1) NULL | сон, часов (v2) |
| `wellbeing` | int NULL CHECK (1–5) | самочувствие (v2) | | `wellbeing` | int NULL CHECK (1–5) | самочувствие 1–5 (Р1-А — поле v1, данные правила 1) |
| `client_guid` | uuid NOT NULL UNIQUE | идемпотентность |
### `src.breaks` — перерывы (Ф-14) ### `src.breaks` — перерывы (Ф-14)
| Поле | Тип | Описание | | Поле | Тип | Описание |
|---|---|---| |---|---|---|
| `id` | serial PK | — | | `id` | uuid PK | UUID — телефон/сервер (Р2-Б) |
| `athlete_id` | int NOT NULL | → `src.athlete.id` | | `athlete_id` | int NOT NULL | → `src.athlete.id` |
| `kind` | text NULL | ответ атлета: `illness` \| `vacation` \| `workload` \| `injury` \| `other`; NULL — ещё не отвечен (правила 12–14 ждут ответа) | | `kind` | text NULL | ответ атлета: `illness` \| `vacation` \| `workload` \| `injury` \| `other`; NULL — ещё не отвечен (правила 12–14 ждут ответа) |
| `source` | text NOT NULL DEFAULT 'auto' | 🛡 всегда `auto` — break вычисляется по пропускам (Ф-14.1) | | `source` | text NOT NULL DEFAULT 'auto' | 🛡 всегда `auto` — break вычисляется по пропускам (Ф-14.1) |
| `started_on` | date NOT NULL | первый пропущенный день | | `started_on` | date NOT NULL | первый пропущенный день |
| `ended_on` | date NULL | возврат (заполняется первой сессией после break) | | `ended_on` | date NULL | возврат (заполняется первой сессией после break) |
| `note` | text | причина-текст пользователя | | `note` | text | причина-текст пользователя |
| `client_guid` | uuid NOT NULL UNIQUE | идемпотентность (для manual) |
### `src.workout_metrics` — метрики тренировки (MetricsEngine, 1:1) ### `src.workout_metrics` — метрики тренировки (MetricsEngine, 1:1)
| Поле | Тип | Описание | | Поле | Тип | Описание |
|---|---|---| |---|---|---|
| `id` | serial PK | — | | `id` | serial PK | — |
| `workout_id` | int NOT NULL UNIQUE | → `src.workout.id` | | `workout_id` | uuid NOT NULL UNIQUE | → `src.workout.id` |
| `athlete_id` | int NOT NULL | изоляция выборок | | `athlete_id` | int NOT NULL | изоляция выборок |
| `adherence` | numeric(4,1) | % закрытых подходов плана (КУ-5) | | `adherence` | numeric(4,1) | % закрытых подходов плана (КУ-5) |
| `tonnage` | numeric(9,1) | Σ вес×повторы, кг | | `tonnage` | numeric(9,1) | Σ вес×повторы, кг |

View File

@ -4,12 +4,17 @@
> Источники: [vision.md](../1-reqs/vision.md) v1.4, > Источники: [vision.md](../1-reqs/vision.md) v1.4,
> [requirements.md](../1-reqs/requirements.md) (утверждён, раздел 7 — > [requirements.md](../1-reqs/requirements.md) (утверждён, раздел 7 —
> открытые вопросы), [architecture.md](../2-arch/architecture.md) > открытые вопросы), [architecture.md](../2-arch/architecture.md)
> (утверждена), дизайн LLM-планирования, документ форматов. > (утверждена), дизайн LLM-планирования, документ форматов,
> UI/UX-рекомендации ([ui-ux-recommendations.md](ui-ux-recommendations.md)).
> Версия 1.1 — **УТВЕРЖДЕНО** (2026-10-17; интеграция документа метрик — > Версия 1.1 — **УТВЕРЖДЕНО** (2026-10-17; интеграция документа метрик —
> Ф-13, Safety-пороги, daily_log — утверждена вместе с документом). > Ф-13, Safety-пороги, daily_log — утверждена вместе с документом).
> Версия 1.2 — черновик (2026-10-17): errata ревью (З-6, З-11) + > Версия 1.2 — черновик (2026-10-17): errata ревью (З-6, З-11) +
> интеграция документа перерывов (Ф-14: сущность `break`, > интеграция документа перерывов (Ф-14: сущность `break`,
> `session_kind`). Утверждение — Пользователь! > `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. Решения открытых вопросов (закрытие раздела 7 requirements) ## 1. Решения открытых вопросов (закрытие раздела 7 requirements)
@ -43,7 +48,9 @@
Ответ: `{ "accepted": ["guid1", ...] }` (все или ничего — пакет Ответ: `{ "accepted": ["guid1", ...] }` (все или ничего — пакет
атомарен; 409 при конфликте версии программы — перезапись на телефоне атомарен; 409 при конфликте версии программы — перезапись на телефоне
после перечитывания). Повтор пакета безопасен: сервер отвечает тем же после перечитывания). Повтор пакета безопасен: сервер отвечает тем же
`accepted` по уже принятым GUID (идемпотентность, Ф-7.4). `accepted` по уже принятым GUID (идемпотентность, Ф-7.4). Р2-Б: PK
клиентских сущностей — UUID (генерирует телефон; строки «только .fit» —
сервер): ссылки дочерних записей — те же UUID, маппинг id не нужен.
### 2.2. Сущности `data` (ключевые поля) ### 2.2. Сущности `data` (ключевые поля)
@ -54,14 +61,15 @@
(normal|return|deload|test — Ф-14.5), `program_version_id` (nullable), (normal|return|deload|test — Ф-14.5), `program_version_id` (nullable),
`avg_hr`; `avg_hr`;
- `workout_set`: `workout_id`, `exercise_id`, `seq`, `weight_kg`, `reps`, - `workout_set`: `workout_id`, `exercise_id`, `seq`, `weight_kg`, `reps`,
`done_at`, `source` (voice|watch|edit), `in_plan` (bool), `rpe` (int? 0–10), `done_at`, `source` (voice|watch|edit), `in_plan` (bool), `rpe` (int? 1–10, З-7),
`knee_pain` (int? 0–10, только группа «ноги»); `knee_pain` (int? 0–10, только группа «ноги»);
- `profile`: `sex`, `birth_year`, `weight_kg`, `hr_max` (nullable); - `profile`: `sex`, `birth_year`, `weight_kg`, `hr_max` (nullable);
- `break`: `kind` (NULL до ответа атлета), `started_on`, `note`, - `break`: `kind` (NULL до ответа атлета), `started_on`, `note`,
`planned_return` — break ставится автоматически (детекция по `planned_return` — break ставится автоматически (детекция по
пропускам ≥ 4 дней), причина запрашивается у атлета (Ф-14.1–2); пропускам ≥ 4 дней), причина запрашивается у атлета (Ф-14.1–2);
- `daily_log`: `date` (YYYY-MM-DD), `bp_sys`, `bp_dias`, `weight_kg` - `daily_log`: `date` (YYYY-MM-DD), `bp_sys`, `bp_dias`, `weight_kg`,
(Ф-13.1; v2 добавит `hr_resting`, `waist_cm`, `sleep_h`, `wellbeing`); `hr_resting`, `wellbeing` (1–5 — Р1-А, поля v1; v2 добавит
`waist_cm`, `sleep_h`);
- `program_version`: JSON-схема программы из дизайна LLM-планирования - `program_version`: JSON-схема программы из дизайна LLM-планирования
(разд. 2) + `athlete_id` (выводится из ключа, в теле не передаётся). (разд. 2) + `athlete_id` (выводится из ключа, в теле не передаётся).
@ -101,6 +109,15 @@
| `Sync.WifiOnly` | bool | — | | `Sync.WifiOnly` | bool | — |
| `Hr.MaxBpm` | int? | 120–240 или пусто (расчёт по Танаке) | | `Hr.MaxBpm` | int? | 120–240 или пусто (расчёт по Танаке) |
| `Rest.DefaultSec` | int | 30–600 | | `Rest.DefaultSec` | int | 30–600 |
| `Safety.BpCancelSys / BpCancelDias` | int | умолчания 150/90 (документ метрик, Р3-А) |
| `Safety.BpCardioSys / BpCardioDias` | int | умолчания 140/90; ≤ BpCancel |
| `Safety.RestingHrSitting` | int | умолчание 90 |
| `Safety.LightWeekHrRise` | int | умолчание 8 |
🛡 Р3-А: телефонные `Safety.*` — источник экрана допуска (офлайн);
серверные `Safety.*` (п. 4.2) — для серверных правил (`red_flags`);
совпадение значений — ответственность владельца конфигурации (общая
синхронизируемая сущность — кандидат v2).
### 4.2. Сервер (`appsettings.json`) ### 4.2. Сервер (`appsettings.json`)
@ -145,7 +162,30 @@
- **HTTPS**: обратный прокси (nginx/Caddy) терминирует TLS; Kestrel — - **HTTPS**: обратный прокси (nginx/Caddy) терминирует TLS; Kestrel —
только localhost — вопрос развёртывания, вне кода. только localhost — вопрос развёртывания, вне кода.
## 6. Чек-лист шага 7 (проверки прототипами) ## 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. 1. Companion-канал: пульс/таймер доходят на телефон (ОВ-3) — итерация 1.
2. Логин Garmin Connect из C# (FitFetcher) — итерация 2. 2. Логин Garmin Connect из C# (FitFetcher) — итерация 2.

View File

@ -0,0 +1,111 @@
# UI/UX-рекомендации для разработки WatchBody
Версия 1.0 · 2026-10-17
Основа: исследование книг и гайдов по wearable- и mobile-UX (Sullivan «Designing for Wearables», Wroblewski «Mobile First», Nielsen/Budiu «Mobile Usability», Krug «Don't Make Me Think», Norman «The Design of Everyday Things», гайды Android Wear OS, ProtoPie, UsabilityGeek, MDPI 2024). Ниже — применение этих принципов к составу WatchBody: приложение на часах (Garmin Fenix 6X, Connect IQ), компаньон на телефоне, веб-админ-консоль владельца.
---
## 1. Общие принципы (все клиенты)
1. **Glanceable, а не readable.** Любой экран читается за 1–2 секунды. Одно число — один экран. В тренажёрном зале пользователь смотрит на часы между подходами, часто в движении и при ярком/тусклом свете.
2. **Одна задача — один поток.** На часах нет «навигации ради навигации»: линейный поток «подход → отдых → следующий подход». Не вкладывать больше двух уровней.
3. **Минимум ввода, максимум предзаполнения.** Вес, повторы, упражнения подставляются из плана и истории; пользователь только подтверждает или корректирует. Правило Круга «Don't Make Me Think»: выбор из списка предпочтительнее ручного ввода.
4. **Ошибкоустойчивость.** Все деструктивные действия (завершить тренировку, удалить подход) — с подтверждением; отмена последнего действия доступна в течение сессии.
5. **Прерывистость — норма.** Тренировка прерывается (подошёл тренер, звонок, отдых затянулся). Состояние сохраняется на часах и синхронизируется; возврат в контекст — не глубже одного экрана.
## 2. Часы (Fenix 6X, Connect IQ)
### 2.1. Аппаратные ограничения учитываем как данность
- Экран 280×280, MIP-дисплей (64 цвета,Palette), читаемость на солнце — плюс; в помещении — высокий контраст, крупные шрифты (минимум ~22–26 px для ключевых цифр; системные шрифтовые константы Connect IQ — `Graphics.FONT_LARGE`, `FONT_NUMBER_*`).
- Взаимодействие: только 5 кнопок (START/BACK/UP/DOWN/LIGHT), тачскрина нет. Каждый экран проектируется под «вверх/вниз — выбор, START — подтвердить, BACK — отмена/выход». Никаких жестов, требующих точности.
- Ремешок: рука вспотевшая, перчатки — целевые зоны кнопок крупные, «случайные» нажатия (LIGHT в кармане) игнорируем/обрабатываем.
### 2.2. Поток тренировки
- **Во время подхода** — не требуем никакого взаимодействия: часы показывают текущее упражнение/сет и вибрируют по завершении отдыха.
- **Ввод RPE/боли по сетам** — упрощённая шкала (вариант кнопками UP/DOWN по значению из плана ±, подтверждение START); значение по умолчанию = последнее для этого упражнения.
- **Отдых между подходами** — крупнейший таймер на экране, вибрация за 5 с и в конце; на том же экране — следующий подход («3×10 · 40 кг»), чтобы атлет готовил вес заранее.
- **Safety-пороги** (по техтребованиям): предупреждение — вибрация + экран с крупным текстом и одной кнопкой «OK»; порог «стоп» — красный/инвертированный экран, требующий явного подтверждения. Никаких предупреждений очередями.
- Экран пробуждения/приветствия перед стартом тренировки: название тренировки дня, кол-во упражнений, ожидаемое время — и START.
### 2.3. Что НЕ делаем на часах
- Планирование, история за период, графики, настройки порогов — всё это на телефоне/вебе. Часы — только исполнение тренировки и подсказки.
- Свободный текстовый ввод — исключён.
## 3. Телефон (компаньон)
- **Mobile-first** (Wroblewski): проектируем от узкого экрана, а не «десктоп, ужатый в телефон».
- **Одна рука**: ключевые действия (старт/пауза тренировки, отметка подхода) — в нижней трети экрана, в зоне большого пальца.
- **Целевая аудитория 45+**: базовый размер шрифта ≥ 16 pt, контраст по WCAG AA (≥ 4.5:1), крупные цели касания ≥ 44×44 pt, без тонких шрифтов и «серого на сером». Это же лечит использование в зале при ярком свете.
- **Ввод данных**: числовые поля — пикеры с шагом (2.5 кг), а не клавиатура; последний использованный вес — значение по умолчанию; для RPE/боли — сегмент-контролы, не слайдеры.
- **Офлайн-first**: все данные тренировки пишутся локально и синхронизируются позже (sync по client_guid/id-mapping — см. архитектуру); UI честно показывает статус синхронизации.
- Уведомления — только значимые: напоминание о тренировке, итоги, нарушение Safety-порога. Никакого «стreak-спама».
## 4. Веб-админ-консоль владельца
- Десктоп-first, но без сломанной верстки на планшете.
- Главные экраны: план на неделю, прогресс (простые таблицы и линейные графики, а не «дашборд из 20 виджетов»), редактирование программы, приём FIT-файлов.
- Подтверждение LLM-плана: diff-представление «что изменилось против предыдущей недели» + явные кнопки «Принять / Отклонить / Править» (Krug: явный выбор вместо подразумеваемого).
- Экспорт/бэкап данных — всегда доступен одной кнопкой.
## 5. Доступность и здоровье (специфика проекта)
- Пользователь 45+, гипертония: крупные цифры ЧСС/давления, красный/зелёный дублируем текстом или иконкой (не только цветом — дальтонизм и яркий свет).
- Все единицы (кг, повторы, пульс, RPE по Боргу) — как в утверждённых метриках проекта; не вводить синонимы на разных экранах.
- Медицинские предупреждения формулировать спокойно и конкретно («Пульс 165 выше порога. Отдохните или завершите»), без тревожной лексики.
## 6. Конкретика из изученных источников
### 6.1. ProtoPie — гайд по smartwatch-UX
- Экран часов — ~30 % площади смартфона: проектируем «at a glance», взаимодействия длительностью в секунды.
- Целевое состояние — **мышечная память**: пользователь действует не глядя. Для Fenix 6X это означает неизменную семантику кнопок на всех экранах (UP/DOWN — выбор, START — подтвердить, BACK — отмена), даже ценой менее «оптимальных» отдельных потоков.
- Избегать иерархических деревьев; работают линейные потоки и страничная модель.
- Анимации — минимум и только лёгкие: тяжёлые анимации тормозят бюджетные устройства и съедают батарею; медиа (например, короткая анимация техники упражнения) — на телефоне, не на часах.
- Мгновенный отклик на любое действие — главный критерий; медленная система на запястье воспринимается как сломанная.
- Не показывать чувствительные данные (давление, пульс покоя) на всплывающих уведомлениях, видимых окружающим.
- Хаптика (вибро-паттерны разных типов: подход выполнен / отдых кончился / предупреждение) — канал информации без затрат экрана.
- Тестировать на реальном железе с ранней стадии, а не только в симуляторе Connect IQ.
### 6.2. Android Wear OS design principles (Material 3 for Wear)
- **Design for critical tasks**: на часах — только 1–2 ключевые задачи (исполнение тренировки + ввод RPE/боли). Всё остальное — на телефоне/вебе.
- **Optimize for the wrist**: задача на часах должна решаться за секунды — иначе эргономический дискомфорт и усталость руки (для атлета под штангой это критично).
- **Glanceable surfaces**: основное содержимое — в виде «взгляд-поверхностей» (у Garmin аналог — data fields в активити и watch face complications: можно вывести «текущее упражнение / подход X из Y» прямо на циферблат в дни тренировок).
- **Always relevant**: контент подстраивается под контекст (время суток, день недели, запланированная тренировка).
- **Works offline**: часы и телефон обязаны полноценно работать без сети (спорт-сценарий) — совпадает с офлайн-first требованием проекта.
- Полезные референсы из этого гайда: non-scrolling layouts для потоков «подход/отдых», confirmation overlay для подтверждений, dialogs/steppers/pickers как канонические компоненты — переносить паттерны на кнопочную навигацию Fenix.
### 6.3. MDPI 2024 — исследование юзабилити смарт-часов
Методика: 11 кастомизированных эвристик (6 из Нильсена: self-descriptiveness, consistency, минимализм, снижение нагрузки на память, соответствие реальному миру, error tolerance; 5 из Motti & Caine для носимых: privacy, ease of use, flexibility/efficiency, features, satisfaction) + SUS-опросник. Результаты: лучшие часы набрали SUS 87,4; чаще всего проблемы обнаруживались именно в «ease of use», «flexibility/efficiency» и «features».
Применяем к WatchBody:
- **Чек-лист эвристик для ревью каждого экрана** — тот же набор из 11 пунктов; баг-репорты по шкале серьёзности 1–5 (косметика → катастрофа).
- **SUS ≥ 80 как целевой порог** качества релиза приложения (средний по индустрии — 68).
- Типовые проблемы из исследования, которых избегаем:
- сообщения об ошибках исчезают быстрее, чем их успевают прочитать (GW4) → все предупреждения Safety закрываются только явным нажатием кнопки;
- нет автоматического переподключения после разрыва связи (типовая жалоба) → устойчивый reconnect часы↔телефон↔сервер и честный индикатор статуса;
- нельзя поменять размер шрифта → настройки размера шрифта на телефоне;
- сброс настроек/сопряжения после обновлений → сохранение конфигурации на сервере (id-mapping), а не только на устройстве.
## 7. Процесс проверки
- Прототип часов проверяем «на запястье»: одна рука занята, экран гаснет, взгляд 2 секунды; симулятор — только для разработки, приёмка — на реальном Fenix 6X (ProtoPie).
- Эвристическое ревью по 11 эвристикам из 6.3 на каждом экране + SUS-опрос после первых юзабилити-сессий; порог релиза SUS ≥ 80.
- Критерий готовности экрана часов: ключевое действие за ≤ 3 нажатий кнопок и ≤ 5 секунд, с работающей мышечной памятью после ≤ 3 сессий.
## 8. Источники
- Scott Sullivan, *Designing for Wearables: Effective UX for Current and Future Devices* — https://www.amazon.com/Designing-Wearables-Effective-Current-Devices-ebook/dp/B01NBNDR5D
- Luke Wroblewski, *Mobile First*; Jakob Nielsen & Raluca Budiu, *Mobile Usability*; Theresa Neil, *Mobile Design Pattern Gallery*; Dan Saffer, *Designing Gestural Interfaces*; Steve Krug, *Don't Make Me Think*; Don Norman, *The Design of Everyday Things*; Wathan & Schoger, *Refactoring UI*; Dannaway, *Practical UI*
- Android Wear OS design principles — https://developer.android.com/design/ui/wear/guides/get-started/design-for-wearables/principles
- Apple HIG (watchOS) — https://developer.apple.com/design/human-interface-guidelines
- ProtoPie, *UX for Wearables: Ultimate Guide* — https://www.protopie.io/blog/ultimate-guide-to-smartwatch-ux
- UsabilityGeek, *Smartwatch UX Design — Top Considerations* — https://usabilitygeek.com/smartwatch-ux-design-top-considerations
- MDPI Future Internet 16(6):204 (2024), *Usability Evaluation of Wearable Smartwatches* — https://www.mdpi.com/1999-5903/16/6/204
- Garmin Connect IQ SDK (UX и дизайн-гайдлайны Fenix 6X) — https://developer.garmin.com/connect-iq/