Files
RuvdsTest/docs/PLAN.md
danamir 942dfc9c1a Initial commit: standalone quiz-testing system
Full-stack F# (Domain/Server/Client via Fable+Elmish+Feliz), PostgreSQL
persistence via Dapper, Docker Compose deployment. Student quiz-taking flow
with time-limit enforcement and focus-loss tracking, Teacher question bank
and quiz builder with results analytics, Admin user management.
2026-08-06 12:36:16 +03:00

188 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# План реализации
Живой чек-лист по `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` уже реализован — но нигде не вызывается, лимит времени сейчас не принуждается.
Все пункты ниже — то, чего в коде пока нет.
## Архитектурный рефакторинг: организация кода по вертикальным срезам
Отдельный от фаз 17 вопрос — не про функциональность, а про то, как раскладывать код по файлам
по мере роста 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/<TypeName>/<Method>` по этим 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/<Role>/<UseCase>.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, фазы 34).
### Когда применять
- [ ] Не блокирует фазы 12 (домен/БД) — там организация по типу файла (`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)