docs(exam-prep): план модуля подготовки к экзамену (generic под несколько экзаменов)
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
# Модуль «Подготовка к экзамену» — план разработки
|
||||
|
||||
**Цель:** превратить пассивный браузер 80 вариантов ЦТ-стиля по математике 9 в полноценный модуль подготовки к экзамену с интерактивным тренажёром, аналитикой и пробниками. Архитектура — generic под несколько экзаменов (математика 9, физика 9, химия 9, ЦТ 11 и т.д.).
|
||||
|
||||
**Старт:** 2026-05-29.
|
||||
**Текущая база:** [/exam9](../frontend/exam9.html) — 80 вариантов × 30 заданий, KaTeX + SVG, чтение + просмотр решения, учительский «назначить как ДЗ» (только нечётные).
|
||||
**Совместимость:** старая страница `exam9.html` сохраняется как «Браузер вариантов» внутри нового модуля (вкладка), URL `/exam9` переадресуется на `/exam-prep/math9/variants`.
|
||||
|
||||
---
|
||||
|
||||
## 0. Generic-архитектура: что значит «несколько экзаменов»
|
||||
|
||||
Модуль параметризуется **ключом экзамена** `examKey`:
|
||||
|
||||
| examKey | Заголовок | Предмет | Класс | Длит. | Вар. | Зад/вар. |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `math9` | Экзамен 9 класс — Математика | math | 9 | 180 мин | 80 | 30 |
|
||||
| `phys9` | Экзамен 9 класс — Физика | phys | 9 | 180 мин | — | — |
|
||||
| `chem9` | Экзамен 9 класс — Химия | chem | 9 | 180 мин | — | — |
|
||||
| `math11ce` | ЦТ — Математика | math | 11 | 180 мин | — | 30 |
|
||||
|
||||
На старте делаем `math9`. Регистрация остальных — это только заполнение `exam_tracks` + добавление сидов задач. Никаких кодовых изменений.
|
||||
|
||||
---
|
||||
|
||||
## 1. Структура URL и страниц
|
||||
|
||||
### Routes (Express)
|
||||
```
|
||||
GET /exam-prep → редирект на /exam-prep/math9 (или список, когда экзаменов >1)
|
||||
GET /exam-prep/:examKey → exam-prep.html (дашборд)
|
||||
GET /exam-prep/:examKey/variants → exam-prep-variants.html (старый /exam9 view)
|
||||
GET /exam-prep/:examKey/variants/:n → exam-prep-variants.html?v=:n
|
||||
GET /exam-prep/:examKey/practice → exam-prep-practice.html (тренажёр случайных задач)
|
||||
GET /exam-prep/:examKey/topics → exam-prep-topics.html (карта тем)
|
||||
GET /exam-prep/:examKey/topics/:slug → exam-prep-topics.html?t=:slug
|
||||
GET /exam-prep/:examKey/mock → exam-prep-mock.html (стартовый экран)
|
||||
GET /exam-prep/:examKey/mock/:mid → exam-prep-mock.html?m=:mid (в процессе/результат)
|
||||
GET /exam9 → 301 redirect /exam-prep/math9/variants (back-compat)
|
||||
```
|
||||
|
||||
В `backend/src/server.js` добавляются 5-6 кастомных роутов, отдающих соответствующий HTML (паттерн как у `/exam9`).
|
||||
|
||||
### Файлы фронта
|
||||
```
|
||||
frontend/
|
||||
exam-prep.html ← дашборд
|
||||
exam-prep-variants.html ← старый exam9.html, перенесённый сюда
|
||||
exam-prep-practice.html ← тренажёр (random)
|
||||
exam-prep-topics.html ← темы (список + по теме)
|
||||
exam-prep-mock.html ← пробный (старт + run + результат)
|
||||
css/exam-prep.css ← общий CSS модуля
|
||||
js/exam-prep/
|
||||
common.js ← общая загрузка examKey, sidebar, helpers
|
||||
api.js ← обёртки LS.api для /api/exam-prep/*
|
||||
task-card.js ← рендер задания с проверкой ответа
|
||||
answer-input.js ← компонент ввода (radio для MC, text для open)
|
||||
katex.js ← обёртка над KaTeX bootstrap
|
||||
progress.js ← вычисление прогресса/streak
|
||||
dashboard.js ← логика exam-prep.html
|
||||
practice.js ← логика exam-prep-practice.html
|
||||
topics.js ← логика exam-prep-topics.html
|
||||
mock.js ← логика exam-prep-mock.html
|
||||
variants.js ← логика exam-prep-variants.html (по сути старый app.js)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. База данных (новые таблицы)
|
||||
|
||||
Все таблицы создаются миграцией `004_exam_prep.sql`. Существующие `exam9_variant_tests` и `feature_exam9_enabled` остаются для совместимости с учительским «назначить как ДЗ».
|
||||
|
||||
```sql
|
||||
-- Реестр экзаменов
|
||||
CREATE TABLE exam_tracks (
|
||||
exam_key TEXT PRIMARY KEY, -- 'math9'
|
||||
title TEXT NOT NULL, -- 'Экзамен 9 класс — Математика'
|
||||
subject_slug TEXT NOT NULL, -- 'math'
|
||||
grade INTEGER NOT NULL, -- 9
|
||||
duration_min INTEGER NOT NULL, -- 180
|
||||
tasks_per_variant INTEGER NOT NULL, -- 30
|
||||
variants_count INTEGER NOT NULL, -- 80
|
||||
scoring_json TEXT, -- JSON: [{correct:30, score:100}, ...] сетка
|
||||
intro_html TEXT, -- описание для лендинга
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- Банк задач (импорт из v*.js)
|
||||
CREATE TABLE exam_tasks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
exam_key TEXT NOT NULL REFERENCES exam_tracks(exam_key) ON DELETE CASCADE,
|
||||
variant INTEGER NOT NULL, -- 1..80
|
||||
task_idx INTEGER NOT NULL, -- 1..30
|
||||
task_type TEXT NOT NULL, -- 'mc' | 'open' | 'long'
|
||||
text_html TEXT NOT NULL, -- условие (HTML с $...$ для KaTeX)
|
||||
figure_html TEXT, -- опциональный SVG/HTML
|
||||
opts_json TEXT, -- JSON: [["а","..."],...] для mc, NULL для остальных
|
||||
answer TEXT, -- 'г' для mc, '-2' / '7500' для open, NULL для long
|
||||
solution_html TEXT NOT NULL, -- полное решение (HTML)
|
||||
topic TEXT, -- 'algebra/powers' (заполнится в фазе 4)
|
||||
subtopic TEXT,
|
||||
difficulty INTEGER, -- 1-5
|
||||
UNIQUE(exam_key, variant, task_idx)
|
||||
);
|
||||
CREATE INDEX idx_exam_tasks_topic ON exam_tasks(exam_key, topic);
|
||||
CREATE INDEX idx_exam_tasks_variant ON exam_tasks(exam_key, variant);
|
||||
|
||||
-- Попытки пользователя
|
||||
CREATE TABLE exam_attempts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
exam_task_id INTEGER NOT NULL REFERENCES exam_tasks(id) ON DELETE CASCADE,
|
||||
user_answer TEXT,
|
||||
is_correct INTEGER, -- 0/1, NULL = долгое задание без автопроверки
|
||||
time_ms INTEGER, -- сколько потратил на эту задачу
|
||||
mode TEXT NOT NULL, -- 'practice'|'variant'|'topic'|'mock'
|
||||
session_id INTEGER, -- группировка попыток в пробнике/тем.сессии
|
||||
hint_used INTEGER NOT NULL DEFAULT 0, -- 0/1, открывал ли подсказку
|
||||
solution_viewed INTEGER NOT NULL DEFAULT 0,-- 0/1, открывал ли решение
|
||||
created_at INTEGER NOT NULL -- unix ms
|
||||
);
|
||||
CREATE INDEX idx_exam_attempts_user_time ON exam_attempts(user_id, created_at DESC);
|
||||
CREATE INDEX idx_exam_attempts_task ON exam_attempts(exam_task_id);
|
||||
CREATE INDEX idx_exam_attempts_session ON exam_attempts(session_id) WHERE session_id IS NOT NULL;
|
||||
|
||||
-- Пробные экзамены
|
||||
CREATE TABLE exam_mock_sessions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
exam_key TEXT NOT NULL,
|
||||
variant INTEGER, -- если из готового варианта
|
||||
source TEXT NOT NULL, -- 'variant' | 'random' | 'weak-topics'
|
||||
task_ids_json TEXT NOT NULL, -- JSON [id,id,...] — порядок задач
|
||||
started_at INTEGER NOT NULL,
|
||||
finished_at INTEGER,
|
||||
duration_planned_min INTEGER NOT NULL, -- 180
|
||||
score INTEGER, -- балл по сетке
|
||||
total_correct INTEGER,
|
||||
total_tasks INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'active' -- 'active'|'finished'|'abandoned'
|
||||
);
|
||||
CREATE INDEX idx_mock_user ON exam_mock_sessions(user_id, started_at DESC);
|
||||
|
||||
-- План подготовки (по дате экзамена)
|
||||
CREATE TABLE exam_user_plan (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
exam_key TEXT NOT NULL,
|
||||
exam_date TEXT NOT NULL, -- YYYY-MM-DD
|
||||
daily_target INTEGER NOT NULL DEFAULT 10, -- задач в день
|
||||
weak_focus INTEGER NOT NULL DEFAULT 1, -- приоритезировать слабые темы
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, exam_key)
|
||||
);
|
||||
|
||||
-- Темы (справочник, заполняется в фазе 4)
|
||||
CREATE TABLE exam_topics (
|
||||
slug TEXT PRIMARY KEY, -- 'algebra-powers'
|
||||
exam_key TEXT NOT NULL,
|
||||
parent_slug TEXT, -- 'algebra' для вложенности
|
||||
title TEXT NOT NULL, -- 'Степени и корни'
|
||||
description TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_topics_exam ON exam_topics(exam_key, parent_slug);
|
||||
```
|
||||
|
||||
### Seed: `exam_tracks`
|
||||
```sql
|
||||
INSERT INTO exam_tracks (exam_key,title,subject_slug,grade,duration_min,tasks_per_variant,variants_count,scoring_json,sort_order)
|
||||
VALUES ('math9','Экзамен 9 класс — Математика','math',9,180,30,80,
|
||||
'[{"correct":30,"score":100},{"correct":28,"score":95},...]', -- сетка перевода
|
||||
10);
|
||||
```
|
||||
|
||||
### Импорт-скрипт
|
||||
`backend/scripts/import-exam-tasks.js` — переносит `frontend/js/exam9/variants/v*.js` в таблицу `exam_tasks`:
|
||||
1. Загружает каждый `v*.js` в изолированный VM-контекст с подставным `VARIANTS = {}`.
|
||||
2. Для каждой задачи парсит:
|
||||
- `text_html`, `figure_html`, `solution_html`, `opts_json` — напрямую.
|
||||
- `task_type` — из наличия `opts`: если есть → `mc`, иначе если в `sol` есть `<div class="sol-ans">Ответ: $число$` → `open`, иначе → `long`.
|
||||
- `answer` — регулярка по `<div class="sol-ans">Ответ:\s*([абвгд])` для MC; `Ответ:\s*\$?(-?[\d,\.]+)\$?` для open; NULL для long.
|
||||
3. **Идемпотентно:** `DELETE FROM exam_tasks WHERE exam_key='math9'` перед вставкой.
|
||||
4. Запуск: `node backend/scripts/import-exam-tasks.js math9`. Без аргумента — все ключи.
|
||||
|
||||
**Качество автопарса:** ожидаемо ~80-90% задач получат корректный `answer`. Остальные требуют:
|
||||
- ручной правки `v*.js` (добавить `answer: "г"` явным полем в task → парсер берёт его в первую очередь),
|
||||
- либо помечаем `task_type='long'` (не идут в тренажёр, только в просмотре варианта).
|
||||
|
||||
Поле `answer` в `v*.js` — необязательное расширение текущего формата. Старый код `exam9.html` его игнорирует.
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend API
|
||||
|
||||
Новый роут: `backend/src/routes/exam-prep.js`, монтируется как `/api/exam-prep`.
|
||||
|
||||
| Метод | Путь | Что делает |
|
||||
|---|---|---|
|
||||
| GET | `/tracks` | список включённых треков (для лендинга) |
|
||||
| GET | `/:examKey/info` | трек + агрегаты пользователя (всего задач, решено, точность) |
|
||||
| GET | `/:examKey/variants` | список вариантов с прогрессом пользователя (для browse-вкладки) |
|
||||
| GET | `/:examKey/variants/:n/tasks` | задачи варианта N (text/figure/opts/answer-shape) |
|
||||
| GET | `/:examKey/topics` | дерево тем + кол-во задач + точность пользователя по теме |
|
||||
| GET | `/:examKey/topics/:slug/tasks?limit=20&exclude_solved=1` | задачи по теме для тренажёра |
|
||||
| GET | `/:examKey/practice/next?count=10&strategy=random\|weak\|unsolved` | выборка задач для тренажёра |
|
||||
| GET | `/:examKey/dashboard` | агрегат: точность, последние попытки, streak, top-3 слабых тем, прогресс по плану |
|
||||
| GET | `/:examKey/plan` | план пользователя |
|
||||
| PUT | `/:examKey/plan` | сохранить план (exam_date, daily_target, weak_focus) |
|
||||
| POST | `/attempts` | сохранить попытку `{exam_task_id, user_answer, is_correct, time_ms, mode, session_id, hint_used, solution_viewed}` |
|
||||
| GET | `/attempts?task_id=X` | история попыток по задаче (для повторного открытия) |
|
||||
| POST | `/:examKey/mock/start` | создать пробник `{source, variant?, count?, weak_topics?}` → `{id, task_ids, duration_min}` |
|
||||
| GET | `/mock/:id` | состояние пробника + задачи (без `answer`/`solution` пока active) |
|
||||
| POST | `/mock/:id/answer` | сохранить ответ (без проверки) `{task_id, user_answer, time_ms}` |
|
||||
| POST | `/mock/:id/finish` | завершить → вернуть результат с `is_correct`/`solution` |
|
||||
| GET | `/mock/:id/result` | результат завершённого пробника (балл + разбор) |
|
||||
| GET | `/admin/tasks/:id` *(admin)* | редактор задачи (для исправлений) |
|
||||
| PUT | `/admin/tasks/:id` *(admin)* | сохранить правки задачи |
|
||||
|
||||
**Ответы безопасны:** при mode=`mock` и статусе active — `answer` и `solution_html` не отдаются. После `finish` отдаются.
|
||||
|
||||
**Производительность:** агрегаты дашборда (точность, слабые темы) считаются одним запросом с GROUP BY, без N+1.
|
||||
|
||||
---
|
||||
|
||||
## 4. Расчёты и формулы
|
||||
|
||||
### Точность
|
||||
```
|
||||
accuracy = correct_attempts / total_attempts
|
||||
```
|
||||
Где `correct_attempts` — последняя попытка по каждой задаче (не «все попытки за всё время»). Это даёт реальный текущий уровень: если ученик ошибся, потом решил — считаем решённой.
|
||||
|
||||
### Слабая тема
|
||||
Тема со ≥3 попыток и точностью < 60%, отсортированы по `(accuracy ASC, attempts DESC)`. Top-3 показываем на дашборде.
|
||||
|
||||
### Streak
|
||||
Подряд идущие календарные дни с ≥1 правильной попыткой. Сегодня обнуляет/сохраняет.
|
||||
|
||||
### Готовность к экзамену (gauge 0-100)
|
||||
```
|
||||
readiness = 0.6 * (coverage) + 0.4 * (accuracy_weighted)
|
||||
coverage = topics_with_3plus_attempts / total_topics
|
||||
accuracy_weighted = avg(accuracy_per_topic) только по тем с ≥3 попытками
|
||||
```
|
||||
Метрика — для дашборда (виджет «Готовность: 73%»). Грубая, мотивационная.
|
||||
|
||||
### План «Сегодня»
|
||||
```
|
||||
days_left = max(1, exam_date - today)
|
||||
tasks_left = total_tasks - distinct_solved_tasks
|
||||
daily_target = clamp(ceil(tasks_left / days_left), 5, 50)
|
||||
weights = если weak_focus=1: 70% задач из top-3 слабых тем, 30% случайных нерешённых
|
||||
```
|
||||
|
||||
### Балл пробника
|
||||
По `scoring_json` из трека (сетка перевода кол-ва правильных в балл). Если NULL — просто процент.
|
||||
|
||||
---
|
||||
|
||||
## 5. Фазы реализации
|
||||
|
||||
| # | Что | Фронт | Бэк | Оценка |
|
||||
|---|---|---|---|---|
|
||||
| **F0** | Миграция `004_exam_prep.sql` + seed `math9` + импорт-скрипт + проверка качества автопарса ответов | — | DB schema, import script | 0.5 дня |
|
||||
| **F1** | Скелет страниц `exam-prep.html` + sidebar (замена `/exam9`), общий CSS, `common.js`, базовый дашборд (статичный) | 5 HTML, общий CSS, sidebar | route handlers (отдача HTML), `/api/exam-prep/tracks`, `/info` | 1 день |
|
||||
| **F2** | Браузер вариантов (перенос текущего `/exam9` в `exam-prep-variants.html`, читает из API а не из `v*.js`) | `variants.js` + рефактор | `/variants`, `/variants/:n/tasks` | 0.5 дня |
|
||||
| **F3** | Компонент `task-card` с вводом ответа + проверкой + `POST /attempts` + просмотром решения после ответа | `task-card.js`, `answer-input.js` | `/attempts` (POST/GET) | 1 день |
|
||||
| **F4** | Дашборд live: точность, последние попытки, streak, прогресс-бар. Реальные данные. | `dashboard.js`, `progress.js` | `/dashboard` | 0.5 дня |
|
||||
| **F5** | Тренажёр «случайные задачи» — выборка через `/practice/next?strategy=random\|unsolved` | `practice.js` | `/practice/next` | 0.5 дня |
|
||||
| **F6** | Тегирование тем: LLM-скрипт классификации + ручная проверка + `exam_topics` справочник | — | `tag-tasks.js` скрипт | 1 день (включая ручную верификацию) |
|
||||
| **F7** | Карта тем + тренажёр по теме | `topics.js`, `topic-card.js` | `/topics`, `/topics/:slug/tasks` | 0.5 дня |
|
||||
| **F8** | Слабые места на дашборде + кнопка «Прокачать тему» + `strategy=weak` | + изменения в dashboard.js/practice.js | расширение `/dashboard`, `/practice/next` | 0.5 дня |
|
||||
| **F9** | Пробный экзамен: старт, таймер, ответы без проверки, финиш, результат с разбором | `mock.js` (3 экрана) | `/mock/start`, `/mock/:id/answer`, `/mock/:id/finish`, `/mock/:id/result` | 1.5 дня |
|
||||
| **F10** | План по дате экзамена: форма даты + виджет «Сегодня N задач» на дашборде | дополнение `dashboard.js` | `/plan` (GET/PUT), интеграция в `/dashboard` | 0.5 дня |
|
||||
| **F11** | Учительская часть: расширить `import-exam9.js` → все 80 вариантов; отчёт класса по темам | таб «Класс» на learners-странице | расширение `/api/classes/:id/exam-stats` | 1 день |
|
||||
| **F12** | Полировка: skeleton-loaders, empty states, mobile UI, achievements (интеграция с gamification) | косметика | — | 0.5 дня |
|
||||
|
||||
**Итого:** ~10 дней работы (одна большая сессия = 1 фаза). Опционально F11+F12 после внутренней приёмки.
|
||||
|
||||
### Параллелизация
|
||||
- F0 → F1 → F2 — последовательно (база).
|
||||
- F3 — после F0.
|
||||
- F4 — после F3.
|
||||
- F5 — после F3.
|
||||
- F6 — независим, можно параллельно с F1-F5.
|
||||
- F7-F8 — после F6.
|
||||
- F9 — после F3.
|
||||
- F10 — после F4.
|
||||
|
||||
---
|
||||
|
||||
## 6. Sidebar — точечные изменения
|
||||
|
||||
В `js/sidebar.js`, группа `content`:
|
||||
```diff
|
||||
- ${L('/exam9', 'clipboard-check', 'Экзамен 9 класс')}
|
||||
+ ${L('/exam-prep/math9', 'clipboard-check', 'Подготовка к экзамену 9')}
|
||||
```
|
||||
Внутри страницы `/exam-prep/math9` — табы или подменю на 4 вкладки: **Дашборд · Варианты · Тренажёр · Темы · Пробник**.
|
||||
|
||||
Когда будут другие экзамены — родительский пункт `Подготовка к экзаменам` (расскрывающаяся группа) с детьми.
|
||||
|
||||
---
|
||||
|
||||
## 7. Подсчёт ответа: алгоритм проверки
|
||||
|
||||
В `task-card.js` при клике «Проверить»:
|
||||
|
||||
| `task_type` | Ввод | Эталон | Проверка |
|
||||
|---|---|---|---|
|
||||
| `mc` | radio выбран `'г'` | `answer='г'` | строгое равенство |
|
||||
| `open` | text input `'7500'` | `answer='7500'` | нормализация: запятая→точка, обрезка пробелов, парсинг как Number, сравнение с `1e-6` погрешностью |
|
||||
| `long` | нет ввода (только «Я решил / Показать решение») | NULL | пользователь сам отмечает правильность |
|
||||
|
||||
Ответы хранятся в `user_answer` строкой как ввёл, `is_correct` — 0/1/NULL.
|
||||
|
||||
---
|
||||
|
||||
## 8. Совместимость и миграция
|
||||
|
||||
1. **URL `/exam9`** → 301 на `/exam-prep/math9/variants` (server.js).
|
||||
2. **localStorage `exam9_progress_v1`** — оставляем, не мигрируем. После запуска прогресс пишется в БД, старый кэш не мешает.
|
||||
3. **localStorage `exam9_last_variant`** — переименовываем в `exam_prep_math9_last_variant`.
|
||||
4. **`exam9_variant_tests`** — не трогаем, учительский «назначить как ДЗ» продолжает работать. В F11 расширяем импорт на 80 вариантов.
|
||||
5. **`v*.js` файлы** — остаются как авторский источник. Импорт-скрипт перегружает таблицу `exam_tasks`. Любые правки в v55.js → `npm run import-exam-tasks math9`.
|
||||
6. **KaTeX** — общий bootstrap через `js/exam-prep/katex.js`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Что не входит в этот план (future)
|
||||
|
||||
- Видео-разборы решений.
|
||||
- Конструктор кастомного варианта учителем.
|
||||
- Лидерборд между учениками.
|
||||
- Adaptive difficulty (карта Лейтнера для задач).
|
||||
- Push в Telegram по плану.
|
||||
- Экспорт пробника в PDF.
|
||||
|
||||
---
|
||||
|
||||
## 10. Зафиксированные решения (2026-05-29)
|
||||
|
||||
| Вопрос | Решение |
|
||||
|---|---|
|
||||
| Откуда брать правильный ответ | **Гибрид**: если в task есть явное поле `answer:` — берём его; иначе парсим из `<div class="sol-ans">` в `sol` регуляркой. Неуспешные парсы логируются — фиксим явным полем. |
|
||||
| Long-задачи (без автопроверки) | В режимах `practice`/`topic` — **фильтр** `task_type != 'long'`. В `variant`/`mock` — присутствуют, ученик после решения **сам отмечает** «решил/не решил». |
|
||||
| Когда задача «решена» | **≥1 правильная попытка за всю историю**. Прогресс не откатывается. Для `long` — если пользователь отметил «решил». |
|
||||
| История попыток | **Все**. Accuracy считаем по последней попытке на задачу, история — для виджета «Последние попытки». |
|
||||
| Иерархия тем | **Двухуровневая**: `algebra` → `algebra-powers`. На UI группируем по родителю, можно выбрать раздел целиком или конкретную подтему. ~6-8 разделов, ~25-30 подтем. |
|
||||
|
||||
---
|
||||
|
||||
## Acceptance criteria (для всего модуля)
|
||||
|
||||
- [ ] Ученик может выбрать тему и прорешать 10 задач с проверкой ответа.
|
||||
- [ ] Точность пользователя обновляется в реальном времени на дашборде.
|
||||
- [ ] Пробник на 180 минут с таймером, балл по сетке ЦТ.
|
||||
- [ ] Top-3 слабых тем на дашборде с кнопкой «Прокачать».
|
||||
- [ ] План по дате экзамена показывает «Сегодня N задач».
|
||||
- [ ] Старая страница вариантов работает без изменений (через новый URL).
|
||||
- [ ] Учитель может назначить любой из 80 вариантов как ДЗ (не только нечётные).
|
||||
- [ ] Архитектура позволяет добавить `phys9` без изменений кода — только сид + импорт.
|
||||
Reference in New Issue
Block a user