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>
116 lines
10 KiB
Markdown
116 lines
10 KiB
Markdown
# 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)
|
||
|
||
```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 должны работать
|
||
- Старые ссылки `<a href="#stats">` (если есть) тоже
|
||
|
||
### Конвенции вновь создаваемых модулей (Phase 2 закреплено)
|
||
|
||
Каждая section:
|
||
```js
|
||
// 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
|
||
// 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.
|