# 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 удалит).
