Files
Maxim Dolgolyov 3f89030b6e feat(admin): Phase 6 sub-commit 2 — remove .user-panel overlay
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>
2026-05-17 00:08:13 +03:00

10 KiB
Raw Permalink Blame History

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 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-style id, обе секции проверяют existence). Mobile ≤768px: actions hidden (row-click overlay остаётся fallback'ом). Backend: NEW DELETE /api/admin/sessions/:id (admin-only) → _deleteSessionTx транзакция: nullify assignment_sessions.session_id, delete user_answers + session_questions (FK CASCADE но делаем explicit для visibility), delete test_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-panel overlay. NEW: frontend/js/admin/sections/user-detail.js (~370L) and frontend/js/admin/sections/session-detail.js (~180L), both IIFE pattern. admin.js has DEEP_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 via udSwitchTab()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/reloadUserPanel JS, toggleDrawer/renderDrawer in sessions.js. Row onclick: openUserPanel(...)AdminRouter.navigate('#users/N'); sessions row → gotoSession(id)AdminRouter.navigate('#sessions/N'). clearUserHistory/toggleBanUser/confirmDeleteUser now use getActiveUid() helper (reads window.activeUid set 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-panel overlay из 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: 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 должны работать
  • Старые ссылки <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_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/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.