226 lines
12 KiB
Markdown
226 lines
12 KiB
Markdown
# Phase 2: Split admin.html → per-section modules
|
||
|
||
**Status:** ✅ Done
|
||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||
**Domain:** frontend
|
||
**Commit:** 92030b4
|
||
|
||
## Objective
|
||
|
||
Разделить монолит `admin.js` (3500L) на per-section модули в `frontend/js/admin/sections/*.js`. После фазы `admin.js` становится оркестратором (~500-800L): он только подключает router, инициализирует общие виджеты (notif, sidebar) и делегирует загрузку section-данных в соответствующий модуль.
|
||
|
||
## Tasks
|
||
|
||
- [x] Создать `frontend/js/admin/sections/` директорию
|
||
- [x] Определить единый паттерн модуля:
|
||
```js
|
||
// js/admin/sections/<name>.js
|
||
(function () {
|
||
'use strict';
|
||
let inited = false;
|
||
const ctx = { user: null, isAdmin: false }; // прокидываем из admin.js
|
||
async function load() { /* существующий loadX код */ }
|
||
window.AdminSections = window.AdminSections || {};
|
||
window.AdminSections.<name> = {
|
||
init: async (sharedCtx) => {
|
||
Object.assign(ctx, sharedCtx);
|
||
if (inited) return; inited = true; await load();
|
||
},
|
||
reload: load,
|
||
};
|
||
})();
|
||
```
|
||
- [x] Извлечь 13 секций (в порядке риска — от меньшего к большему):
|
||
- [x] `stats.js` — `loadStats` + связанные функции (50L)
|
||
- [x] `sublog.js` — submission log (104L)
|
||
- [x] `sims.js` (118L), `games.js` (132L), `tpl.js` (73L) — admin-only
|
||
- [x] `subjects.js` — настройка доступных тестов (338L)
|
||
- [x] `permissions.js` (68L)
|
||
- [x] `shop.js` — items + purchases + award coins (207L)
|
||
- [x] `gam.js` — gamification stats + award xp (183L)
|
||
- [x] `assignments.js` (477L)
|
||
- [x] `tests.js` (283L)
|
||
- [x] `questions.js` — самая большая, 535L (включая Q-modal)
|
||
- [x] `users.js` — users-table + pagination + user-panel (343L, overlay остался)
|
||
- [x] `sessions.js` — sessions-table + session detail (159L)
|
||
- [x] Модифицировать `admin.js`:
|
||
- Удалить функции, перенесённые в sections
|
||
- Заменить inline вызовы (`loadUsers()` → `AdminSections.users.init()`)
|
||
- Добавить ROUTE_TO_SECTION mapping (см. ниже)
|
||
```js
|
||
const ROUTE_TO_SECTION = {
|
||
stats: 'stats', users: 'users', sessions: 'sessions',
|
||
questions: 'questions', tests: 'tests', assignments: 'assignments',
|
||
subjects: 'subjects', permissions: 'permissions',
|
||
shop: 'shop', gam: 'gam', tpl: 'tpl', sims: 'sims', games: 'games', sublog: 'sublog',
|
||
};
|
||
```
|
||
Маппинг применяется внутри `switchTab` (не отдельный router-listener) —
|
||
`switchTab` уже вызывается router'ом на change через `activate(route)`,
|
||
поэтому достаточно один раз dispatch'ить в `switchTab`.
|
||
- [x] Все 14 `<script>` тегов добавлены в `admin.html` (_shared.js + 13 sections, после router.js, перед admin.js)
|
||
- [x] Глобальные функции, которые используются из onclick HTML, оставлены доступными через `window.X`:
|
||
- `changeRole`, `openUserPanel`, `goAddQuestion`, `confirmDeleteUser`, etc.
|
||
- Каждый section module экспортирует свои onclick-handler'ы через `window.X = X` в конце IIFE
|
||
- Cross-section orchestrator `goAddQuestion` живёт в admin.js (вызывает `AdminSections.questions.openModal`)
|
||
- [x] Удалены per-tab `*Inited` флаги из admin.js — они переехали внутрь section modules
|
||
|
||
## Files to Modify/Create
|
||
|
||
- `frontend/js/admin/sections/stats.js` — новый
|
||
- `frontend/js/admin/sections/users.js` — новый, ~400-500L
|
||
- `frontend/js/admin/sections/sessions.js` — новый
|
||
- `frontend/js/admin/sections/questions.js` — новый, ~800L
|
||
- `frontend/js/admin/sections/tests.js` — новый
|
||
- `frontend/js/admin/sections/assignments.js` — новый
|
||
- `frontend/js/admin/sections/subjects.js` — новый
|
||
- `frontend/js/admin/sections/permissions.js` — новый
|
||
- `frontend/js/admin/sections/shop.js` — новый
|
||
- `frontend/js/admin/sections/gam.js` — новый
|
||
- `frontend/js/admin/sections/tpl.js` — новый
|
||
- `frontend/js/admin/sections/sims.js` — новый
|
||
- `frontend/js/admin/sections/games.js` — новый
|
||
- `frontend/js/admin/sections/sublog.js` — новый
|
||
- `frontend/js/admin/admin.js` — сильно ужать (с 3500L до ~500-800L)
|
||
- `frontend/admin.html` — добавить 13 `<script>` тегов
|
||
|
||
## Acceptance Criteria
|
||
|
||
- `admin.js` ≤ 800L (от 3500L)
|
||
- Каждый section-файл ≤ 900L (questions.js самый большой)
|
||
- Все 13 табов работают идентично текущему поведению (no regressions)
|
||
- Cross-tab handlers (`goAddQuestion`, `confirmDelete*`) работают
|
||
- Lazy-load работает: при первом открытии tab делается fetch, при повторном — нет
|
||
- F5 на любом `#X` корректно ленево-грузит секцию (через router из Phase 1)
|
||
- Browser back/forward работают
|
||
- Никаких console errors в Devtools
|
||
|
||
## Notes
|
||
|
||
### Стратегия извлечения
|
||
|
||
Один section за раз, мелкими безопасными шагами:
|
||
1. Скопировать функции `loadX, openXModal, deleteX, ...` в новый файл sections/<name>.js, обернуть в IIFE
|
||
2. Экспортировать через `window.AdminSections.X`
|
||
3. Подключить `<script>` в admin.html
|
||
4. В admin.js заменить вызовы (`loadX()` → `AdminSections.X.init(ctx)`)
|
||
5. Удалить дубликаты в admin.js
|
||
6. Тест: открыть tab — работает?
|
||
7. Перейти к следующей секции
|
||
|
||
### Что НЕ переезжает в sections
|
||
|
||
- `LS.initPage()` + auth check — остаётся в admin.js
|
||
- `switchTab` (helper) — остаётся
|
||
- `pctClass`, `fmtDate`, `fmtTime` — общие утилиты, остаются (или переезжают в `admin/_shared.js`)
|
||
- Sidebar / notif init — остаётся
|
||
- Router setup — остаётся
|
||
|
||
### Глобальные функции из onclick
|
||
|
||
Сейчас многие функции вызываются из HTML onclick (`onclick="openUserPanel(...)"`). Чтобы не переписывать HTML на этой фазе, в каждом section module экспортируем нужные функции через `window.X = X` внутри IIFE. Phase 5/6 могут заменить onclick на event delegation, но Phase 2 этого не делает (incremental).
|
||
|
||
### Тестирование каждой секции
|
||
|
||
После каждой выделенной секции:
|
||
- Открыть `/admin` → переключиться на этот tab → данные загрузились
|
||
- Все кнопки/модалки секции работают
|
||
- Cross-tab navigation (если есть) работает
|
||
- F5 на `#<route>` корректно открывает tab
|
||
|
||
Если регрессия — откатить эту итерацию, разобраться, починить.
|
||
|
||
### Совет implementer'у
|
||
|
||
Если фаза становится огромной — можно сделать несколько коммитов внутри phase branch. Это inscope. Не нужно делать один гигантский коммит на 14 файлов.
|
||
|
||
## Review Checklist
|
||
|
||
- [x] Все 13 секций имеют одинаковую структуру (init/reload)
|
||
- [x] admin.js = 701L (≤ 800L), нет дублирования с sections
|
||
- [x] Все window.X экспорты есть для onclick handlers (см. handoff ниже)
|
||
- [x] Lazy-init работает: `inited` флаг внутри каждой section IIFE
|
||
- [x] F5 на каждом из 13 routes восстанавливает секцию (через router.activate → switchTab → AdminSections.X.init)
|
||
- [x] Sanity: все 14 .js файлов проходят `node --check`
|
||
|
||
## Handoff to Next Phase
|
||
|
||
### Section module API (Phase 3+ должна следовать)
|
||
|
||
```js
|
||
window.AdminSections.<name> = {
|
||
init: async () => { /* lazy: первый вызов делает fetch, повторные — no-op */ },
|
||
reload: async () => { /* всегда выполняет fetch (для refresh-кнопок) */ },
|
||
// опционально: extra-методы для cross-section orchestration:
|
||
// openModal(...), loadModalTopics() — см. questions.js
|
||
};
|
||
```
|
||
|
||
### Где живут shared утилиты
|
||
|
||
`frontend/js/admin/_shared.js` — экспортирует на `window.AdminCtx`:
|
||
- `user`, `isTeacher`, `isAdmin` (filled by admin.js после LS.initPage())
|
||
- `MODES`, `DIFFS`, `DIFF_LABELS`, `TYPE_LABELS` — константы
|
||
- `pctClass(p)`, `fmtDate(d)`, `fmtTime(s)`, `fmtDuration(s)` — форматтеры
|
||
- `renderMath(el)` — KaTeX
|
||
- `qTypeBadge(type)`, `qOptsPreview(q)` — used by tests + subjects
|
||
- `renderPgnControls(elId, page, total, perPage, gotoFn)` + `ensurePgnStyles()` — пагинация
|
||
|
||
### ROUTE_TO_SECTION map (admin.js)
|
||
|
||
13 ключей, расширение для Phase 3 (`overview`) и Phase 6 (`user`, `session` deep pages):
|
||
|
||
```js
|
||
const ROUTE_TO_SECTION = {
|
||
stats:'stats', questions:'questions', tests:'tests',
|
||
assignments:'assignments', subjects:'subjects', users:'users',
|
||
sessions:'sessions', permissions:'permissions', shop:'shop',
|
||
gam:'gam', tpl:'tpl', sims:'sims', games:'games', sublog:'sublog',
|
||
};
|
||
```
|
||
|
||
**Phase 3:** добавить `overview: 'overview'` + sections/overview.js, и поменять
|
||
дефолтный hash в admin.js с `#stats` на `#overview`.
|
||
|
||
**Phase 6:** добавить `user: 'userDetail'`, `session: 'sessionDetail'` —
|
||
sections/user-detail.js и sections/session-detail.js будут читать
|
||
`AdminRouter.current().params[0]` для id.
|
||
|
||
### Window-exposed globals (для HTML onclicks)
|
||
|
||
**Из admin.js (orchestrator):**
|
||
- `switchTab`, `toggleAdminGroup`, `goAddQuestion`
|
||
- Topics: `showAddTopic`, `createTopic`, `renameTopic`, `deleteTopic`
|
||
- Logs/health: `sendBroadcast`, `clearAuditLog`, `clearErrorLog`
|
||
- Classroom: `crMasterToggle`, `crHistDebounce`, `loadCrHistory`,
|
||
`toggleCrDetail`, `adminEndSession`, `adminExportChat`, `adminDeleteSession`
|
||
- Avatars: `avatarApprove`, `avatarRejectPrompt`, `avatarReject`
|
||
- Function declarations at script-top-level (`loadTopics`, `loadCrActiveSessions`,
|
||
`loadAvatarRequests`, `loadHealth`, `loadAuditLog`, `loadErrorLog`, `loadCrModuleState`,
|
||
`loadCrSessionDetail`, `loadTopicSubjects`, `renderCrPagination`, `fmtLiveDuration`,
|
||
`avatarReject`) — автоматически на `window` в non-module script.
|
||
|
||
**Из section modules (явный `window.X = X` в IIFE):**
|
||
- questions: ~25 handlers (openQModal, saveQuestion, setQType, addOpt, removeOpt, etc.)
|
||
- users: ~17 handlers (loadUsers, openUserPanel, changeRole, etc.)
|
||
- tests: ~12 handlers
|
||
- assignments: ~18 handlers
|
||
- subjects: ~9 handlers (toggleScCard, applyPreset, scAddQ/scRemoveQ, etc.)
|
||
- shop, gam, sims, games, tpl, permissions, sessions, sublog: 1-9 each
|
||
|
||
### Cross-section dependencies
|
||
|
||
- `goAddQuestion(slug)` → admin.js orchestrator → switches tab + calls
|
||
`AdminSections.questions.openModal()` + `loadModalTopics()`. The section
|
||
exposes these as extra methods on `AdminSections.questions`.
|
||
- Subjects-section's `goAddQuestion(slug)` onclick uses the admin.js
|
||
orchestrator (same window-global).
|
||
- `_matchPairs` (matching-question editor state): exposed on `window` because
|
||
inline `oninput="window._matchPairs[i].left=this.value"` references it.
|
||
|
||
### Что НЕ переехало (по плану)
|
||
|
||
- Tabs *topics, audit, errors, health, classroom, avatars* остались inline
|
||
в admin.js — Phase 2 их не extract'ил (не входило в 13 секций плана).
|
||
- `.user-panel` overlay markup в admin.html не тронут (Phase 6 удалит).
|