# Архитектура и границы первой версии ## Стек .NET 10, ASP.NET Core, Blazor, EF Core, PostgreSQL. Один процесс приложения и отдельный контейнер БД. Без микросервисов. ## Зависимости - Domain: простые предметные классы, без зависимостей от UI и хранилища. - Application → Domain: прикладные сценарии и контракты хранилища. - Infrastructure → Application: EF Core, PostgreSQL, авторизация и внешние сервисы. - Web → Application + Infrastructure: интерфейс и регистрация зависимостей. Infrastructure подключается в Web для сборки приложения. Компоненты работают через контракты Application. ## Программа Клиент принадлежит тренеру. У клиента несколько сохранённых месячных программ. Программа содержит недели, недели — тренировки, тренировки — упорядоченные блоки. Блок содержит ровно одно упражнение. Количество подходов (`SetsCount`) и отдых в минутах (`RestMinutes`) находятся у блока. У упражнения общие текстовые повторения и вес, текстовые разминка и примечание, nullable decimal процент от максимума, название, ссылка и флаг камеры. Процент вводит тренер; автоматического соответствия между повторениями и процентом нет. Сеты не поддерживаются. Общий отдых вычисляется как сумма `SetsCount × RestMinutes` всех блоков, включая отдых после последнего подхода. Текст повторений на эту формулу не влияет. Длительность тренировки (`Duration`) вводится тренером текстом и не вычисляется из отдыха. Миграция ExercisePrescription переносит общие вес и повторения из первого подхода, секунды делит на 60, старые сеты разделяет на отдельные блоки без перестановки упражнений. Перед применением нужна резервная копия. Откат с восстановлением индивидуальных подходов возможен только из резервной копии; автоматический Down запрещён. Упражнение программы хранит снимок названия и ссылки из библиотеки. Изменение библиотеки не изменяет старые программы. Копирование создаёт новые идентификаторы всех элементов программы. История — список сохранённых программ клиента, а не журнал каждого нажатия клавиши. ## Конфигурация Настраиваемые значения задаются в appsettings.json и переопределяются стандартным провайдером переменных окружения .NET. В именах переменных `__` разделяет секции. Docker Compose читает `.env` и передаёт переменные контейнеру. Сам .NET файл `.env` автоматически не читает. Секреты не имеют рабочих значений по умолчанию и не хранятся в Git. Версии инструментов, имена маршрутов, CSS и структура данных не являются runtime-настройками. ## Этапы 1. Основа решения, правила разработки, предметные классы и конфигурация. 2. PostgreSQL, авторизация, клиенты и библиотека упражнений. 3. Редактор месячной программы и независимое копирование. 4. PDF и проверка полного сценария в Docker. 5. Адаптация под iPhone 15 — после первой версии. ## Сохранение и безопасность UI использует Blazor Interactive Server. Каждая операция хранилища создаёт отдельный DbContext; общий контекст на долгоживущий Blazor circuit не используется. Репозитории проверяют тренера, полученного из серверного состояния авторизации. Экспорт получает идентификатор тренера из авторизованного HTTP-запроса. Сохранение небольшой месячной программы заменяет её внутренние дочерние записи одной транзакцией. Идентификаторы недель, тренировок и блоков являются внутренними и не используются в ссылках. Это сохраняет простую реализацию для первого этапа; при необходимости частичных обновлений контракт репозитория можно сохранить. Идентификатор программы стабилен, `Version` защищает от перезаписи из другой вкладки. История программ не удаляется. Автосохранение срабатывает после редактирования с задержкой из конфигурации. Незаполненное упражнение остаётся локальным черновиком до заполнения обязательных полей. Статус сохранения и ошибка видны в интерфейсе. Переход со страницы с несохранёнными данными требует подтверждения. ASP.NET Core Identity: хеширование паролей, блокировка повторных неудачных входов, antiforgery для форм, HTTP-only cookie и периодическая проверка security stamp. Регистрации нет. Первый тренер создаётся из конфигурации только в пустой базе. Для HTTPS за прокси задаётся адрес доверенного прокси; произвольные forwarded-заголовки не принимаются. PDF генерируется PDFsharp/MigraDoc, шрифты с кириллицей устанавливаются в Docker-образ. Каждая неделя — одна непрерывная альбомная A4-таблица. Общие заголовки недели и столбцов повторяются на продолжении страниц; тренировки разделяются цветными строками и имеют собственные итоги. Заголовок тренировки удерживается вместе с первой строкой, итог — с последней. Размеры, цвета и пути задаются в Pdf. Ссылки являются PDF-аннотациями; значок камеры встроен как статический рисунок и не зависит от emoji-шрифтов ОС. ## Публикация Blazor в Docker В Web-проекте явно установлен `RequiresAspNetWebAssets=true`: при отдельном Docker-слое восстановления зависимостей Razor-файлы ещё не скопированы, поэтому автоматическое обнаружение компонентов не подходит. Dockerfile дополнительно проверяет наличие `wwwroot/_framework/blazor.web.js` после публикации. Это настройка сборки, а не runtime-параметр appsettings. Документация: https://learn.microsoft.com/aspnet/core/blazor/project-structure?view=aspnetcore-10.0 ## Группы библиотеки У `LibraryExercise` необязательная ссылка `GroupId` на `ExerciseGroup`, обе записи принадлежат тренеру. Упражнения внутри разных групп независимы. Добавление из другой группы создаёт новую запись с копией названия и ссылки. Перетаскивание меняет только `GroupId`, не переписывая остальные поля. `null` означает «Без группы». Удаление группы каскадно удаляет только её упражнения библиотеки. Снимки упражнений в программах не имеют внешнего ключа на библиотеку, поэтому история не меняется. Все операции проверяют тренера и у источника, и у целевой группы. Миграция ExerciseGroups добавляет таблицу групп и nullable GroupId: прежние упражнения остаются без группы. Поиск фильтрует по всем словам без учёта порядка, регистра и «ё/е»; точные совпадения и совпадения с начала имени выше совпадений внутри имени. В списке показана группа, чтобы различать независимые упражнения с одинаковыми названиями. Компонент ExercisePicker использует всплывающий список поверх страницы: ширина соответствует полю, направление раскрытия и высота зависят от доступного места окна. Нативный popover закрывается по Escape и клику снаружи. Двойной клик и Enter вызывают явный OnApply, одиночный клик меняет выбор; флаг применения предотвращает повторное добавление во время операции.