69 lines
11 KiB
Markdown
69 lines
11 KiB
Markdown
# Архитектура и границы первой версии
|
||
|
||
## Стек
|
||
|
||
.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, одиночный клик меняет выбор; флаг применения предотвращает повторное добавление во время операции.
|