Files
Learn_System/plans/admin-redesign/CONTEXT.md
T
Maxim Dolgolyov 69113ab35e feat(admin): phase 5 — per-row quick actions for users + sessions
Hover-only action buttons (right-aligned, opacity transition, hidden on mobile).

- users.js: 4 actions (ban/unban, award coins, sessions, delete) — replaces `>` glyph cell, falls back to glyph for non-admin / self

- sessions.js: 2 actions (view, delete)

- DELETE /api/admin/sessions/:id (NEW): transactional (assignment_sessions=NULL, user_answers, session_questions, test_sessions), audit-logged, admin-only

- event.stopPropagation defence-in-depth (each button + parent .row-actions)

- LS.confirm for destructive ops; LS.modal for award-coins amount/reason

- CSS injected once via #row-actions-style id-dedup (same content in both sections)

Existing user-panel overlay + session toggle-drawer flows untouched (Phase 6 removes overlay).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:53:19 +03:00

105 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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 должны работать
- Старые ссылки `<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.