# Feature Context: Admin Panel Redesign ## Current State (будет обновляться после каждой фазы) - ✅ Phase 1 implemented — `window.AdminRouter` обёртывает старый `switchTab` (hash ↔ tab двусторонне). `switchTab` принимает 2-й аргумент `{ fromRouter: true }` для предотвращения рекурсии. Default = `#stats`. Файлы: `frontend/js/admin/router.js` (новый), `frontend/admin.html` (+1 строка), `frontend/js/admin/admin.js` (модификация `switchTab` + IIFE `initAdminRouter`). - ✅ Phase 2 implemented (commit 92030b4) — admin.js ужат с ~3591L до 701L. Все 13 plan-tabs живут в `frontend/js/admin/sections/*.js` (IIFE pattern) + `frontend/js/admin/_shared.js` (window.AdminCtx). switchTab() диспетчит в `AdminSections[ROUTE_TO_SECTION[name]].init()`. Lazy-load работает (inited флаг внутри каждой IIFE). System tabs (topics/audit/errors/health/classroom/avatars) остались inline в admin.js — Phase 2 их не extract'ил. - ✅ Phase 3 implemented — `#overview` стал дефолтным route'ом admin-панели. Backend: `GET /api/admin/overview` (admin-only, ~0.08ms/call) возвращает digest за 24ч: новые регистрации, запущенные сессии, активные юзеры, активные классы, failed-сессии, забаненные за неделю (из audit log), топ-5 завершённых сессий. Frontend: `frontend/js/admin/sections/overview.js` (~205L) рендерит bento-grid карточки + alerts + топ-таблицу + quick-links (deep-link через `AdminRouter.navigate`). `admin.js`: дефолт `'stats'` → `'overview'` в `activate()`, initial nav, и initial init. Old `#stats` остался работающим (доступен через nav-item). Файлы: `frontend/js/admin/sections/overview.js` (NEW), `backend/src/controllers/adminController.js` (+57L: `overviewStmts` + `getOverview`), `backend/src/routes/admin.js` (+1L), `js/api.js` (+1 helper), `frontend/admin.html` (nav-item + tab-pane + script tag), `frontend/js/admin/admin.js` (ROUTE_TO_SECTION + default route refs). - ✅ Phase 4 implemented — Cmd+K (Ctrl+K) global command palette. Backend: `GET /api/admin/search?q=X` (admin-only) returns `{users[5], tests[3], classes[3]}` via 3 prepared LIKE queries (`title AS name` for tests, `invite_code AS code` for classes). Frontend: `frontend/js/admin/palette.js` (~320L) — custom modal (NOT LS.modal) with capture-phase Ctrl+K listener that `stopImmediatePropagation`'s to override `/js/search.js`. Debounced 150ms, ↑↓ Enter Esc keyboard nav, click-outside close. Action registry (8 entries) is hardcoded — extend by appending to `ACTIONS` const. Result interactions: user → `AdminRouter.navigate('#users/' + id)` (Phase 6 deep page hook), test → `#tests`, class → `/classes#id`. Exposed: `window.AdminPalette = { open, close, isOpen }`, `LS.adminGlobalSearch(q)`. Files: `frontend/js/admin/palette.js` (NEW), `backend/src/controllers/adminController.js` (+50L: `searchStmts` + `globalSearch`), `backend/src/routes/admin.js` (+1L), `js/api.js` (+4L helper + export), `frontend/admin.html` (+1 script tag). - ⬜ Phase 5-6 not started ## Temporary Workarounds (пусто — заполняется implementer'ом) ## Cross-Phase Dependencies - **Phase 2 depends on Phase 1:** sections подписываются на router events, чтобы lazy-init по hashchange - **Phases 3, 4, 5 depend on Phase 2:** новые модули будут добавляться в `js/admin/sections/` (структура из фазы 2) - **Phase 6 depends on Phase 2:** deep page для user/session — это новые sections в той же структуре - **Phase 6 removes** старую `.user-panel` overlay из admin.html — фазы 1-5 НЕ должны её удалять ## Router Contract (Phase 1) ```js // Subscribe in any future module: AdminRouter.on('change', ({ route, params, raw }) => { /* ... */ }); // Programmatic deep-link without polluting history: AdminRouter.navigate('#users/123', { replace: true, silent: true }); ``` - Events emitted: `'change'` only (payload: parsed route). - Late subscribers do NOT receive replay — call `AdminRouter.current()` on init. - `silent: true` suppresses the synchronous emit but native `hashchange` still fires; the internal `_navigating` flag in router.js prevents the listener from re-firing. - `switchTab(btn, { fromRouter: true })` — call from router handlers to skip the reverse-sync write to `location.hash` (avoids redundant `replaceState`). ## Implementation Notes ### Существующая структура (что менять / что НЕ менять) **Точки входа в admin.js:** - `LS.initPage()` — auth + role check - `switchTab(btn)` — текущий tab-роутер; будет обёрнут router'ом, но не удалён до фазы 6 - Per-tab `*Inited` флаги (`usersInited`, `sessionsInited`, ...) — переедут в section modules **Backward compat обязателен:** - `goAddQuestion(slug)` и подобные cross-tab onclick handlers должны работать - Старые ссылки `` (если есть) тоже ### Конвенции вновь создаваемых модулей (Phase 2 закреплено) Каждая section: ```js // js/admin/sections/.js (function () { 'use strict'; let inited = false; async function load() { /* fetch + render */ } // Optional onclick handlers used by HTML / dynamic templates: window.handlerX = handlerX; window.AdminSections = window.AdminSections || {}; window.AdminSections. = { init: async () => { if (inited) return; inited = true; await load(); }, reload: load, // Optional extras for cross-section calls (e.g. questions.openModal): // openModal: (...) => { ... }, }; })(); ``` Shared utilities — на `window.AdminCtx` (см. `_shared.js`): - `user`, `isTeacher`, `isAdmin` (filled by admin.js) - `MODES`, `DIFFS`, `DIFF_LABELS`, `TYPE_LABELS` - `pctClass`, `fmtDate`, `fmtTime`, `fmtDuration` - `renderMath`, `qTypeBadge`, `qOptsPreview` - `renderPgnControls`, `ensurePgnStyles` ROUTE_TO_SECTION map в admin.js — добавлять новые ключи при добавлении секций (Phase 3 = `overview`, Phase 6 = `user`/`session` deep pages). Router (фаза 1): ```js // js/admin/router.js window.AdminRouter = { navigate(hash) { /* update hash + dispatch */ }, current() { /* parse current hash */ }, on(event, fn) { /* subscribe */ }, }; ``` ### Какие onclick handlers есть сейчас (выборка) Из admin.html / admin.js: - `onclick="switchTab(this)"` — на каждой admin-nav-item - `onclick="openUserPanel(event, ${u.id}, '${u.role}')"` — на user row - `onclick="changeRole(this)"` — на role-select - `onclick="goAddQuestion('${slug}')"` — cross-tab Эти должны работать без изменений до фазы 6.