# План реализации Живой чек-лист по `docs/DESIGN.md`. Отмечайте пункты по мере реализации (`[ ]` → `[x]`); статусы ниже соответствуют фактическому состоянию кода на 2026-08-03. Фазы и нумерация разделов совпадают с `docs/DESIGN.md` §7 — там же обоснование каждого пункта, здесь только чек-лист. ## Базовое состояние на сегодня - [x] Студенческий флоу целиком на **старой** доменной модели (`Course`/`CategoryId`/без `Composition`) работает end-to-end через in-memory `Store`: `login` → `getAvailableQuizzes` → `startAttempt` → `submitAnswer` → `finishAttempt` — проверено в браузере. - [x] CORS для dev настроен верно (`Client:Origin = http://localhost:5173`), баг с портом 5174 исправлен. - [x] `Grading.applyGradingMethod` уже реализован и покрыт тестами (`HighestAttempt`, пустой список) — но никуда не подключён (нет API-метода, который бы его вызывал). - [x] `Attempt.isExpired` уже реализован — но нигде не вызывается, лимит времени сейчас не принуждается. Все пункты ниже — то, чего в коде пока нет. ## Архитектурный рефакторинг: организация кода по вертикальным срезам Отдельный от фаз 1–7 вопрос — не про функциональность, а про то, как раскладывать код по файлам по мере роста Server/Client. Возник из обсуждения 2026-08-03: сейчас архитектура классическая слоистая (3 проекта = 3 слоя), а не по фиче. **Текущее состояние.** `Domain` (чистые типы и бизнес-логика) → `Server` (`Store.fs` + `Auth.fs` + один `QuizApi.fs` на все хендлеры, роутинг через Fable.Remoting.Giraffe по единому интерфейсу `IQuizApi`) → `Client` (Elmish MVU: один общий `Model` в `Types.fs`, один общий `Msg`-DU, один `update` в `State.fs`, один `View.fs`). Срез идёт по техническому слою (типы / состояние / отображение / API), а не по фиче — ровно то, что архитектура вертикальных срезов (VSA) устраняет. **Почему не полный переход на VSA.** Два места будут этому сопротивляться: 1. Контракт `IQuizApi`/`ITeacherApi`/`IAdminApi` в `Domain.Contracts` (DESIGN.md §4) — это по сути RPC-интерфейс "один record на роль", а не роутинг по фиче. Fable.Remoting-style клиент (`Api.fs`) уже завязан на паттерн `/api//` по этим record'ам. 2. Elmish с одним `Model`/`Msg` на всё приложение — тоже принципиально центральный паттерн; срез по фиче потребовал бы отдельного рефакторинга на суб-модели (паттерн "Elmish page"), не связанного с серверным вопросом. **Компромиссное решение.** Не ломать контракт и Elmish целиком, а **реализацию** каждого метода интерфейса выносить в свой файл/папку по фиче, а не копить всё в одном `TeacherApi.fs`/`View.fs`. Даёт большую часть пользы VSA (не лазить по всему файлу ради одной фичи, фича = один файл со всем необходимым) без переписывания транспорта и стейт-менеджмента. **Почему сейчас удобный момент.** Из нового дизайна пока не реализовано практически ничего (см. «Базовое состояние» выше) — так что переход дешевле всего до того, как `TeacherApi.fs`/`View.fs` разрастутся под Teacher/Admin функциональность. ### Server: организация по фиче - [ ] Вместо одного `TeacherApi.fs`/`AdminApi.fs` — папка `Server/Features//.fs` (например `Server/Features/Teacher/CreateQuiz.fs`, `.../PublishQuiz.fs`), каждый файл содержит маппинг запроса/ответа и логику ровно одного метода интерфейса. - [ ] Сборка `ITeacherApi`/`IAdminApi` в одном месте (аналог текущего `QuizApi.build`) остаётся тонкой композицией — record просто ссылается на функции из `Features/*`, сам не содержит логики. - [ ] Тот же принцип для уже существующего `QuizApi.fs` — постепенно разнести на `Server/Features/Student/*.fs`, но не отдельным рывком, а по мере следующих правок этого файла. ### Client: частичная декомпозиция Elmish - [ ] Общий `Model`/`Msg`/`update` в `Types.fs`/`State.fs` остаётся корнем (сессия, роутинг между страницами), но крупные разделы (кабинет Teacher, кабинет Admin) выносятся в под-модели по паттерну "Elmish page" — свои Model/Msg/update/view на страницу — вместо бесконечного расширения единых `Types.fs`/`State.fs`/`View.fs`. - [ ] Не переписывать существующий студенческий флоу (`Types.fs`/`State.fs`/`View.fs`) ради этого — он маленький и рабочий; применять паттерн к новым разделам (Teacher/Admin UI, фазы 3–4). ### Когда применять - [ ] Не блокирует фазы 1–2 (домен/БД) — там организация по типу файла (`Users.fs`/`Quizzes.fs`/..., `Store.fs`) уже естественна и менять её не нужно. - [ ] Применяется как соглашение **начиная с фазы 3** (Admin API) — новый код сразу пишется в `Features/`-структуре; уже написанный `QuizApi.fs` не рефакторится превентивно, только когда до него дойдёт очередная правка (рефакторинг не ради рефакторинга). ## Фаза 1 — Домен (DESIGN.md §3) ### 1.1 Пользователь (§3.1) - [ ] `User.IsActive` - [ ] `User.CreatedAt` - [ ] `login` проверяет `IsActive`, отказывает тем же текстом ошибки, что и неверный пароль ### 1.2 Темы и вопросы (§3.2) - [ ] `QuestionCategory` → `Topic`, `CategoryId` → `TopicId` - [ ] `Topic.OwnerId` (вместо `CourseId`), `Topic.CreatedAt` - [ ] `Question.TopicId` (вместо `CategoryId`) - [ ] `Question.IsArchived`, `Question.CreatedAt` - [ ] Правило мягкого удаления вопроса (архивировать вместо удаления, если используется) — сама флаг-логика в домене; серверная проверка "используется ли где-то" — фаза 4 ### 1.3 Состав теста (§3.3) - [ ] `RandomTopicRule` - [ ] `QuizComposition` (`FixedQuestions` / `RandomFromTopics`) - [ ] `Quiz.OwnerId` (вместо `CourseId`) - [ ] `Quiz.AssignedStudentIds` - [ ] `Quiz.IsPublished`, `Quiz.IsArchived`, `Quiz.CreatedAt` - [ ] `Quiz.totalPoints` пересчитан под `Composition` (сумма по `FixedQuestions` или `Count * PointsPerQuestion` по `RandomFromTopics`) - [ ] Удалены `Course`, `Enrollment`, `CourseId`, `EnrollmentRole` ### 1.4 Резолв и попытка (§3.4, §3.6) - [ ] `AttemptFinishReason` (`ManualSubmit` / `TimedOut`) - [ ] `Attempt.AttemptNumber` - [ ] `Attempt.FinishReason` - [ ] `Attempt.ResolvedQuestions` - [ ] `Attempt.FocusLossCount` - [ ] `Quiz.resolveComposition` (чистая функция, shuffle и вопросы темы — параметрами) - [ ] `Attempt.submit` принимает `AttemptFinishReason` - [ ] `Grading.gradeAttempt` переключён на `attempt.ResolvedQuestions` вместо `quiz.Questions` - [ ] Проверка `Attempt.isExpired` встроена в поток `submitAnswer`/`finishAttempt` (авто-завершение с `FinishReason = TimedOut`) ### 1.5 Типы аналитики и валидация (§3.5, §3.7) - [ ] `QuestionStat` - [ ] `AttemptSummary`, `AttemptDetail`, `StudentQuizResult` - [ ] `Validation.fs`: `QuizValidation` переработан под `QuizComposition` (сейчас требует непустой `quiz.Questions`, нужно — непустой `FixedQuestions` **или** хотя бы одно правило с `Count > 0` в `RandomFromTopics`) ### 1.6 Юнит-тесты (`tests/Domain.Tests`) - [ ] `resolveComposition`: `FixedQuestions` (с шаффлом и без), `RandomFromTopics` (успешный набор, ошибка при нехватке вопросов в теме) - [ ] `totalPoints` для обоих режимов `Composition` - [ ] `applyGradingMethod`: добавить `AverageAttempt`/`FirstAttempt`/`LastAttempt` (сейчас покрыт только `HighestAttempt`) - [ ] Существующие `GradingTests.fs`/`ValidationTests.fs` обновлены под новую форму `Question`/`Quiz` (`TopicId` вместо `CategoryId`, `Composition` вместо `Questions`) ## Фаза 2 — PostgreSQL (§5) - [ ] SQL-схема: `users`, `topics`, `questions`, `quizzes`, `quiz_assignments`, `attempts`, `attempt_grades` - [ ] Миграции (DbUp, пронумерованные `.sql`, применяются при старте сервера) - [ ] Dapper-репозиторий взамен `Store.fs` (тот же member-интерфейс) - [ ] Строка подключения к Postgres в конфиге (`appsettings.*.json`) - [ ] `IHostedService` — "expiry sweeper" для заброшенных просроченных попыток (§3.6) ## Фаза 3 — Admin API + UI (§2, §4.3) - [ ] `IAdminApi.listUsers` - [ ] `IAdminApi.createUser` (с выбором `Role`) - [ ] `IAdminApi.updateUser` - [ ] `IAdminApi.deactivateUser` - [ ] `IAdminApi.resetPassword` - [ ] Проверка роли `Admin` на сервере для всех методов `IAdminApi` - [ ] UI: список пользователей, создание/деактивация/сброс пароля ## Фаза 4 — Teacher API + UI (§4.2) - [ ] `ITeacherApi`: `listTopics`/`createTopic`/`renameTopic`/`deleteTopic` - [ ] `ITeacherApi`: `listQuestions`/`createQuestion`/`updateQuestion`/`deleteQuestion` (с архивированием вместо удаления, если вопрос используется — §3.2) - [ ] `ITeacherApi`: `listMyQuizzes`/`getQuiz`/`createQuiz`/`updateQuiz` - [ ] `ITeacherApi`: `publishQuiz`/`unpublishQuiz` - [ ] `ITeacherApi`: `deleteQuiz` (архивирование, если есть попытки — §3.3) - [ ] `ITeacherApi`: `listStudents`/`assignStudents`/`unassignStudent` - [ ] Проверка владения (`OwnerId`) на каждом хендлере, кроме `listStudents` - [ ] UI: темы → вопросы → конструктор теста (fixed-список / random-по-темам) → назначение студентов ## Фаза 5 — Результаты и аналитика (§3.5, §3.7, §4.2) - [ ] Запись в `attempt_grades` при `finishAttempt`/авто-завершении - [ ] `ITeacherApi.getQuizAttempts` - [ ] `ITeacherApi.getAttemptDetail` - [ ] `ITeacherApi.getQuizResults` (сводка по студентам с учётом `GradingMethod`) - [ ] `ITeacherApi.getQuestionStats` - [ ] UI: список попыток (с `FocusLossCount`/`FinishReason`/длительностью), карточка попытки, сводная ведомость по студентам, аналитика по вопросам ## Фаза 6 — Student UI (§4.1, §6) - [ ] `getAvailableQuizzes`: фильтр по `IsPublished`, `AssignedStudentIds`, `not IsArchived`, окну дат - [ ] `IQuizApi.getMyResults` + экран истории попыток студента - [ ] `IQuizApi.reportFocusLoss` + клиентские слушатели `visibilitychange`/`blur` - [ ] Обратный отсчёт времени в UI прохождения теста - [ ] Кнопка «Завершить тест» вынесена в отдельный зафиксированный блок, не участвующий в скролле списка вопросов (§6) ## Фаза 7 — Деплой на RuVDS (§1, §7) - [ ] systemd-юнит для Kestrel - [ ] nginx как reverse proxy + TLS - [ ] Прод-конфиг: `Jwt:Secret`, `Client:Origin`, строка подключения к Postgres (сейчас в `appsettings.Development.json` — dev-значения, включая исправленный порт 5173) ## Открытые вопросы (DESIGN.md §8) — решить по ходу соответствующей фазы - [ ] Нужен ли предпросмотр/тестовый прогон теста преподавателем без сохранения попытки в статистику? (фаза 4) - [ ] Показывать ли студенту его ответы с правильными при просмотре результата, или только баллы? (фаза 5/6) - [ ] Валидировать нехватку вопросов в теме для `RandomFromTopics` при создании теста или только при старте попытки? (фаза 1/4) - [ ] Нужен ли визуальный порог/бейдж «подозрительно» при большом `FocusLossCount`? (фаза 5) - [ ] Показывать ли преподавателю архивные вопросы/тесты в общих списках (приглушённо+фильтр) или полностью прятать? (фаза 4)