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

15 KiB
Raw Permalink Blame History

План реализации

Живой чек-лист по docs/DESIGN.md. Отмечайте пункты по мере реализации ([ ][x]); статусы ниже соответствуют фактическому состоянию кода на 2026-08-03. Фазы и нумерация разделов совпадают с docs/DESIGN.md §7 — там же обоснование каждого пункта, здесь только чек-лист.

Базовое состояние на сегодня

  • Студенческий флоу целиком на старой доменной модели (Course/CategoryId/без Composition) работает end-to-end через in-memory Store: logingetAvailableQuizzesstartAttemptsubmitAnswerfinishAttempt — проверено в браузере.
  • CORS для dev настроен верно (Client:Origin = http://localhost:5173), баг с портом 5174 исправлен.
  • Grading.applyGradingMethod уже реализован и покрыт тестами (HighestAttempt, пустой список) — но никуда не подключён (нет API-метода, который бы его вызывал).
  • 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)

  • QuestionCategoryTopic, CategoryIdTopicId
  • 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)