3f89030b6e
Now that the deep pages (sub-commit 1) work, retire the legacy
.user-panel inline overlay entirely.
* admin.html: removed <div class="user-panel" id="user-panel"> block
inside #tab-users, removed dead .user-panel* CSS (kept .btn-close
for any external use).
* users.js: removed openUserPanel / closeUserPanel / reloadUserPanel
and their closure state (activeTr, activeUserRole). User row onclick
switched from openUserPanel(...) → AdminRouter.navigate('#users/N').
clearUserHistory / toggleBanUser / confirmDeleteUser / openEditUserModal
/ openUserPermsModal / doSet/doReset* all refactored to use the
getActiveUid() helper (reads window.activeUid, set by user-detail.init)
+ reloadDetailAndList() helper (refreshes deep page + list together).
* sessions.js: row click + eye-button switched from toggleDrawer(id)
→ gotoSession(id) → AdminRouter.navigate('#sessions/N'). Removed
toggleDrawer + renderDrawer functions (~60L) and openDrawerId state.
Inline drawer markup removed from the row template.
Verified node --check on all touched JS. ast-index confirms zero
remaining usages of openUserPanel / closeUserPanel / reloadUserPanel /
toggleDrawer across the repo.
This completes Phase 6 and the admin-redesign feature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
10 KiB
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+ IIFEinitAdminRouter). - ✅ 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 namefor tests,invite_code AS codefor classes). Frontend:frontend/js/admin/palette.js(~320L) — custom modal (NOT LS.modal) with capture-phase Ctrl+K listener thatstopImmediatePropagation's to override/js/search.js. Debounced 150ms, ↑↓ Enter Esc keyboard nav, click-outside close. Action registry (8 entries) is hardcoded — extend by appending toACTIONSconst. 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 implemented — per-row hover quick actions для users + sessions tables. Users row (admin && uid !== self): 4 кнопки (Ban/Unban toggle, Award coins via LS.modal с amount+reason, Sessions → AdminRouter.navigate('#sessions'), Delete). Sessions row: 2 кнопки (View → toggleDrawer, Delete). Все
event.stopPropagation()чтобы не триггерить row-click overlay/drawer. CSS injected ONCE черезensureRowActionsStyles()(de-dup по#row-actions-styleid, обе секции проверяют existence). Mobile ≤768px: actions hidden (row-click overlay остаётся fallback'ом). Backend: NEWDELETE /api/admin/sessions/:id(admin-only) →_deleteSessionTxтранзакция: nullifyassignment_sessions.session_id, deleteuser_answers+session_questions(FK CASCADE но делаем explicit для visibility), deletetest_sessions. Audit log:'session.delete'. Файлы:frontend/js/admin/sections/users.js(343→469L, +126),frontend/js/admin/sections/sessions.js(159→210L, +51),backend/src/controllers/adminController.js(+27L:_deleteSessionTx+deleteSession),backend/src/routes/admin.js(+1L),js/api.js(+1 helper + export). NO эмоджи, inline SVG (Lucide outline-style 24x24 viewBox), Lucide уже доступен через CDN. User-panel overlay НЕ удалена — оставлена для Phase 6. - ✅ Phase 6 implemented (sub-commits
bd30200+ new) — deep entity pages replace legacy.user-paneloverlay. NEW:frontend/js/admin/sections/user-detail.js(~370L) andfrontend/js/admin/sections/session-detail.js(~180L), both IIFE pattern.admin.jshasDEEP_ROUTES = { users:'user-detail', sessions:'session-detail' }+activateDeepPane();activate(route, params)checks for first-param to dispatch deep page (parent nav-item stays highlighted). Sub-tabs (overview/sessions/classes/audit) with URL sync viaudSwitchTab()→AdminRouter.navigate('#users/N/<sub>', { replace: true, silent: true }). Backend endpoints reused:GET /api/admin/users/:id/sessions(user history),GET /api/admin/sessions/:id(session detail),GET /api/admin/audit-log?limit=500(client-side filtered by uid for Audit tab). Removed:<div class="user-panel" id="user-panel">overlay HTML,.user-panel*CSS,openUserPanel/closeUserPanel/reloadUserPanelJS,toggleDrawer/renderDrawerin sessions.js. Row onclick:openUserPanel(...)→AdminRouter.navigate('#users/N'); sessions row →gotoSession(id)→AdminRouter.navigate('#sessions/N').clearUserHistory/toggleBanUser/confirmDeleteUsernow usegetActiveUid()helper (readswindow.activeUidset by user-detail.init) instead of overlay closure.quickOpenUserSessions(uid)→#users/<uid>/sessions(deep page, Sessions sub-tab). Classes sub-tab is placeholder (no per-user classes endpoint exists). Charts: simple inline SVG bar chart for per-subject avg %.
Phase 6 Routes Glossary
#users— list (Phase 2 section)#users/123— deep page, default Overview sub-tab#users/123/sessions— deep page, Sessions sub-tab#users/123/classes— deep page, Classes sub-tab (placeholder)#users/123/audit— deep page, Audit sub-tab (admin only)#sessions— list (Phase 2 section)#sessions/456— deep page- Cmd+K palette user pick →
#users/N(opens deep page)
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-paneloverlay из admin.html — фазы 1-5 НЕ должны её удалять
Router Contract (Phase 1)
// 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: truesuppresses the synchronous emit but nativehashchangestill fires; the internal_navigatingflag in router.js prevents the listener from re-firing.switchTab(btn, { fromRouter: true })— call from router handlers to skip the reverse-sync write tolocation.hash(avoids redundantreplaceState).
Implementation Notes
Существующая структура (что менять / что НЕ менять)
Точки входа в admin.js:
LS.initPage()— auth + role checkswitchTab(btn)— текущий tab-роутер; будет обёрнут router'ом, но не удалён до фазы 6- Per-tab
*Initedфлаги (usersInited,sessionsInited, ...) — переедут в section modules
Backward compat обязателен:
goAddQuestion(slug)и подобные cross-tab onclick handlers должны работать- Старые ссылки
<a href="#stats">(если есть) тоже
Конвенции вновь создаваемых модулей (Phase 2 закреплено)
Каждая section:
// js/admin/sections/<name>.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.<name> = {
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_LABELSpctClass,fmtDate,fmtTime,fmtDurationrenderMath,qTypeBadge,qOptsPreviewrenderPgnControls,ensurePgnStyles
ROUTE_TO_SECTION map в admin.js — добавлять новые ключи при добавлении секций
(Phase 3 = overview, Phase 6 = user/session deep pages).
Router (фаза 1):
// 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-itemonclick="openUserPanel(event, ${u.id}, '${u.role}')"— на user rowonclick="changeRole(this)"— на role-selectonclick="goAddQuestion('${slug}')"— cross-tab
Эти должны работать без изменений до фазы 6.