12 KiB
Phase 2: Split admin.html → per-section modules
Status: ✅ Done
Parent plan: 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
- Создать
frontend/js/admin/sections/директорию - Определить единый паттерн модуля:
// 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, }; })(); - Извлечь 13 секций (в порядке риска — от меньшего к большему):
stats.js—loadStats+ связанные функции (50L)sublog.js— submission log (104L)sims.js(118L),games.js(132L),tpl.js(73L) — admin-onlysubjects.js— настройка доступных тестов (338L)permissions.js(68L)shop.js— items + purchases + award coins (207L)gam.js— gamification stats + award xp (183L)assignments.js(477L)tests.js(283L)questions.js— самая большая, 535L (включая Q-modal)users.js— users-table + pagination + user-panel (343L, overlay остался)sessions.js— sessions-table + session detail (159L)
- Модифицировать
admin.js:- Удалить функции, перенесённые в sections
- Заменить inline вызовы (
loadUsers()→AdminSections.users.init()) - Добавить ROUTE_TO_SECTION mapping (см. ниже)
Маппинг применяется внутри
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.
- Все 14
<script>тегов добавлены вadmin.html(_shared.js + 13 sections, после router.js, перед admin.js) - Глобальные функции, которые используются из 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)
- Удалены per-tab
*Initedфлаги из admin.js — они переехали внутрь section modules
Files to Modify/Create
frontend/js/admin/sections/stats.js— новыйfrontend/js/admin/sections/users.js— новый, ~400-500Lfrontend/js/admin/sections/sessions.js— новыйfrontend/js/admin/sections/questions.js— новый, ~800Lfrontend/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 за раз, мелкими безопасными шагами:
- Скопировать функции
loadX, openXModal, deleteX, ...в новый файл sections/.js, обернуть в IIFE - Экспортировать через
window.AdminSections.X - Подключить
<script>в admin.html - В admin.js заменить вызовы (
loadX()→AdminSections.X.init(ctx)) - Удалить дубликаты в admin.js
- Тест: открыть tab — работает?
- Перейти к следующей секции
Что НЕ переезжает в sections
LS.initPage()+ auth check — остаётся в admin.jsswitchTab(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
- Все 13 секций имеют одинаковую структуру (init/reload)
- admin.js = 701L (≤ 800L), нет дублирования с sections
- Все window.X экспорты есть для onclick handlers (см. handoff ниже)
- Lazy-init работает:
initedфлаг внутри каждой section IIFE - F5 на каждом из 13 routes восстанавливает секцию (через router.activate → switchTab → AdminSections.X.init)
- Sanity: все 14 .js файлов проходят
node --check
Handoff to Next Phase
Section module API (Phase 3+ должна следовать)
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)— KaTeXqTypeBadge(type),qOptsPreview(q)— used by tests + subjectsrenderPgnControls(elId, page, total, perPage, gotoFn)+ensurePgnStyles()— пагинация
ROUTE_TO_SECTION map (admin.js)
13 ключей, расширение для Phase 3 (overview) и Phase 6 (user, session deep pages):
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 + callsAdminSections.questions.openModal()+loadModalTopics(). The section exposes these as extra methods onAdminSections.questions.- Subjects-section's
goAddQuestion(slug)onclick uses the admin.js orchestrator (same window-global). _matchPairs(matching-question editor state): exposed onwindowbecause inlineoninput="window._matchPairs[i].left=this.value"references it.
Что НЕ переехало (по плану)
- Tabs topics, audit, errors, health, classroom, avatars остались inline в admin.js — Phase 2 их не extract'ил (не входило в 13 секций плана).
.user-paneloverlay markup в admin.html не тронут (Phase 6 удалит).