@startuml hld ' ============================================================================ ' Феникс 6X — high level design (диаграмма развертывания). ' Нотация: A -> B — зависит/использует; A <>-> B — содержит/управляет. ' Цвета (d-drg-4): жёлтый #FFF4D6 — внешнее; серый #BFBFBF — не в v1. ' ============================================================================ title Феникс 6X: часы + телефон атлета + личный сервер\n(v1.0; серым — v2, жёлтым — внешние системы) skinparam shadowing false skinparam defaultTextAlignment center skinparam backgroundColor #FFFFFF skinparam defaultFontName Segoe UI skinparam defaultFontSize 12 skinparam ArrowColor #444444 skinparam component { BackgroundColor #F5F7FA BorderColor #7A8899 } skinparam database { BackgroundColor #F5F7FA BorderColor #7A8899 } together { node "Часы атлета\n(Garmin, Connect IQ)" as Watch { component "FenixDataField\nполе данных «Феникс»:\nпульс, зоны, подходы, вибро" as DataField } node "Телефон атлета\n(Android, Kotlin)" as Phone { component "VoiceInput\nофлайн-STT + грамматика команд" as VoiceInput component "TrainingUi\nэкран тренировки, таймеры, история" as TrainingUi component "WorkoutCore\nлогика тренировки и программ" as WorkoutCore component "LocalDb\nRoom/SQLite" as LocalDb component "SyncQueue\nочередь + идемпотентность" as SyncQueue component "WatchChannel\ncompanion-канал Connect IQ" as WatchChannel } cloud "Личный сервер\n(C# / .NET 10)" as Server { package "WebApp\nASP.NET Core" as WebApp { component "RestApi\nsync, workouts, programs\n(X-Api-Key атлета)" as RestApi component "AdminWeb\nконсоль владельца\n(логин/пароль)" as AdminWeb } package "FitPipeline\nзабор и парсинг" as FitPipeline { component "FitFetcher\nклиент Garmin Connect\n(+ папка импорта)" as FitFetcher component "FitParser\nGarmin FIT SDK" as FitParser } component "MetricsEngine\nadherence, тоннаж, 1ПМ" as MetricsEngine component "ProgressionRules\nправила прогрессии → draft" as ProgressionRules component "ProgramStore\nверсии программ (JSON)" as ProgramStore database "PostgreSQL\nслои src | ref" as Db } component "LlmPlanner\nеженедельный контур LLM" as LlmPlanner #BFBFBF cloud "Garmin Connect\n(облако, .fit)" as GarminConnect #FFF4D6 } ' — зависимости / использование DataField --> WatchChannel : пульс, зоны,\nтаймер отдыха WatchChannel --> TrainingUi : live-данные\n(если связь есть) VoiceInput --> TrainingUi : команды TrainingUi --> WorkoutCore WorkoutCore --> LocalDb LocalDb --> SyncQueue SyncQueue --> RestApi : HTTPS\nPOST /sync FitFetcher --> GarminConnect : .fit по расписанию FitParser --> Db : workout_facts MetricsEngine --> Db ProgressionRules --> ProgramStore : draft-версия ProgramStore --> Db RestApi --> Db AdminWeb --> Db RestApi --> ProgramStore : активная\nверсия LlmPlanner ..> MetricsEngine : сводка недели\n(v2) LlmPlanner ..> ProgramStore : предложение\n(v2) legend bottom |= Связь |= Значение | | ""A -> B"" | A зависит от / использует B | | ""A ..> B"" | связь версии v2 (в v1 не реализована) | | жёлтый | внешняя система | | серый | задумано, не в v1 | endlegend note bottom of Watch Инвариант: часы автономны — запись .fit ведёт платформа Garmin, поле — только отображение. Один код поля — сборки под модели Garmin. end note note bottom of Phone Инвариант офлайн-first: все данные сначала в LocalDb; сеть — только SyncQueue. Каждой записи — client_guid. end note note bottom of Db Инвариант изоляции атлетов: ключ → athlete_id, все выборки — по атлету. end note @enduml