diff --git a/plans/admin-redesign/CONTEXT.md b/plans/admin-redesign/CONTEXT.md new file mode 100644 index 0000000..73e3ad6 --- /dev/null +++ b/plans/admin-redesign/CONTEXT.md @@ -0,0 +1,70 @@ +# Feature Context: Admin Panel Redesign + +## Current State + +(будет обновляться после каждой фазы) + +- ⬜ Phase 1 not started — старый switchTab всё ещё единственный роутер +- ⬜ Phase 2 not started — все 13 секций в admin.js монолите +- ⬜ Phase 3-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 НЕ должны её удалять + +## 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 должны работать +- Старые ссылки `` (если есть) тоже + +### Конвенции вновь создаваемых модулей + +Каждая section (фаза 2): +```js +// js/admin/sections/.js +(function () { + 'use strict'; + let inited = false; + async function load() { /* ... */ } + window.AdminSections = window.AdminSections || {}; + window.AdminSections. = { + init: async () => { if (inited) return; inited = true; await load(); }, + reload: load, + }; +})(); +``` + +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. diff --git a/plans/admin-redesign/PLAN.md b/plans/admin-redesign/PLAN.md new file mode 100644 index 0000000..638e9be --- /dev/null +++ b/plans/admin-redesign/PLAN.md @@ -0,0 +1,84 @@ +# Feature: Admin Panel Redesign + +**Branch:** `feature/admin-redesign` +**Base branch:** `master` +**Created:** 2026-05-16 +**Status:** 🟡 In Progress +**Strategy:** Incremental +**Mode:** Automated +**Execution:** Orchestrator + +## Summary + +Превратить admin-панель LearnSpace из монолитного tab-роутера (1900L HTML + 3500L JS в одном модуле) в master-detail SPA с hash-routing, lazy-loaded per-section модулями, dashboard-landing, Cmd+K command palette, per-row quick actions и deep entity pages вместо overlay-панели. + +**Текущее состояние:** +- `frontend/admin.html` ~1900L +- `frontend/js/admin/admin.js` ~3500L (после недавнего extract из inline `` в `` или перед admin.js +- `frontend/js/admin/admin.js` — модифицировать `switchTab` + добавить init-логику (~15-25L изменений) + +## Acceptance Criteria + +- F5 на `http://localhost:3000/admin#users` восстанавливает users-tab +- Browser back/forward переключают между табами (без полного reload) +- Клик по admin-nav-item обновляет URL (`#users` появляется в адресной строке) +- Клик по cross-tab handler типа `goAddQuestion('bio')` — старая логика работает, URL обновляется +- Unknown hash (например `#nonexistent`) → console.warn + fallback на `#stats`, нет crash +- `#users/123` парсится корректно (params=['123']), но пока никто его не использует — Phase 6 подключит + +## Notes + +### Почему hash-router, а не history.pushState + +Backend Express раздаёт admin.html по `/admin`. С `pushState` пришлось бы либо настраивать catch-all route на server-стороне (`/admin/*`), либо делать SPA-style роутинг. Hash-router работает out-of-the-box и не требует backend-изменений. Это критично для incremental-стратегии — мы не трогаем server в Phase 1. + +### Защита от рекурсии + +Сценарий: пользователь кликает на tab → switchTab вызывает navigate → navigate меняет hash → срабатывает hashchange → router emits 'change' → handler вызывает switchTab → snake eats tail. + +Решение: +```js +let _navigating = false; +function navigate(hash) { + _navigating = true; + location.hash = hash; + _navigating = false; +} +window.addEventListener('hashchange', () => { + if (_navigating) return; + // emit 'change' +}); +``` + +Или передавать `{ silent: true }` через объект-параметр и проверять его в handler'е switchTab. + +### Существующий пример hashchange + +В `frontend/js/textbook-tracker.js:438` уже есть `addEventListener('hashchange', handleHashNav)` — это safe-pattern, можно подсмотреть структуру. + +## Review Checklist + +- [ ] router.js не использует Grep / эмоджи / отсутствующие LS-помощники +- [ ] Старый switchTab НЕ удалён, только обёрнут +- [ ] Нет регрессий: все 13 табов переключаются, lazy-load работает +- [ ] F5 / back / forward проверены вручную в браузере (или симуляция через subagent) +- [ ] Default route `#stats` срабатывает при пустом hash +- [ ] Unknown route не крашит панель +- [ ] Код следует конвенциям проекта (no emoji, inline SVG для иконок, LS.* для API) +- [ ] Build passes: `cd backend && npm start` → http://localhost:3000/admin загружается + +## Handoff to Next Phase + + diff --git a/plans/admin-redesign/phase-2-split-sections.md b/plans/admin-redesign/phase-2-split-sections.md new file mode 100644 index 0000000..8ec7397 --- /dev/null +++ b/plans/admin-redesign/phase-2-split-sections.md @@ -0,0 +1,151 @@ +# Phase 2: Split admin.html → per-section modules + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** frontend + +## Objective + +Разделить монолит `admin.js` (3500L) на per-section модули в `frontend/js/admin/sections/*.js`. После фазы `admin.js` становится оркестратором (~500-800L): он только подключает router, инициализирует общие виджеты (notif, sidebar) и делегирует загрузку section-данных в соответствующий модуль. + +## Tasks + +- [ ] Создать `frontend/js/admin/sections/` директорию +- [ ] Определить единый паттерн модуля: + ```js + // js/admin/sections/.js + (function () { + 'use strict'; + let inited = false; + const ctx = { user: null, isAdmin: false }; // прокидываем из admin.js + async function load() { /* существующий loadX код */ } + window.AdminSections = window.AdminSections || {}; + window.AdminSections. = { + init: async (sharedCtx) => { + Object.assign(ctx, sharedCtx); + if (inited) return; inited = true; await load(); + }, + reload: load, + }; + })(); + ``` +- [ ] Извлечь 13 секций (в порядке риска — от меньшего к большему): + - [ ] `stats.js` — `loadStats` + связанные функции (small, ~50L) + - [ ] `sublog.js` — submission log (medium) + - [ ] `sims.js`, `games.js`, `tpl.js` — admin-only (small каждая) + - [ ] `subjects.js` — настройка доступных тестов + - [ ] `permissions.js` + - [ ] `shop.js` — items + purchases + award coins + - [ ] `gam.js` — gamification stats + award xp + - [ ] `assignments.js` + - [ ] `tests.js` + - [ ] `questions.js` — самая большая, ~800L (включая Q-modal) + - [ ] `users.js` — users-table + pagination + user-panel (overlay остаётся!) + - [ ] `sessions.js` — sessions-table + session detail +- [ ] Модифицировать `admin.js`: + - Удалить функции, перенесённые в sections + - Заменить inline вызовы (`loadUsers()` → `AdminSections.users.init(ctx)`) + - Добавить генератор route→section маппинга: + ```js + const ROUTE_TO_SECTION = { + stats: 'stats', users: 'users', sessions: 'sessions', + questions: 'questions', tests: 'tests', assignments: 'assignments', + subjects: 'subjects', permissions: 'permissions', + shop: 'shop', gam: 'gam', tpl: 'tpl', sims: 'sims', games: 'games', sublog: 'sublog', + }; + AdminRouter.on('change', ({ route }) => { + const sec = ROUTE_TO_SECTION[route]; + if (sec && AdminSections[sec]) AdminSections[sec].init(sharedCtx); + }); + ``` +- [ ] Все 13 `` + +## Acceptance Criteria + +- Ctrl+K (Cmd+K) открывает palette из любого таба admin +- Esc закрывает +- Печать "иван" → top users с именем "Иван..." +- Печать "монеты" → action "Выдать монеты" +- ↑↓ навигация работает, Enter выполняет +- Поиск отрабатывает <100ms для 8 результатов на тестовой БД +- Click outside / Esc закрывают +- LS.modal используется (не reinventing wheel) +- Auth: только admin может открыть (teachers — палетту не открывают) + +## Notes + +### Почему Ctrl+K а не / + +Ctrl+K — индустри-стандарт (GitHub, Linear, Vercel, Slack). `/` конфликтует с input'ами. + +### Дебаунсинг + +Простой setTimeout/clearTimeout. Без библиотек. + +### LS.modal compat + +LS.modal сейчас принимает `{ title, body, footer, onOk, onClose, size }`. Для palette нужен focus management — autofocus input при открытии. Можно использовать через колбэк `onMount` если он есть, либо `setTimeout(() => input.focus(), 0)` после открытия. + +### Что НЕ делать в этой фазе + +- Не делать ML/fuzzy-search в backend (LIKE достаточно) +- Не делать historic recents (Cmd+K recents) — это уже после merge +- Не делать collaboration ("кто-то ещё печатает") + +## Review Checklist + +- [ ] Ctrl+K не конфликтует с системными shortcut'ами браузера +- [ ] Palette не открывается если фокус в textarea / input (если требует ввод)... опционально, можно открывать всегда +- [ ] No SQL injection в /admin/search +- [ ] Эскейпинг через LS.esc для рендеринга имён пользователей +- [ ] No N+1 queries (один SELECT на тип сущности) +- [ ] Build passes + +## Handoff to Next Phase + + diff --git a/plans/admin-redesign/phase-5-quick-actions.md b/plans/admin-redesign/phase-5-quick-actions.md new file mode 100644 index 0000000..52add13 --- /dev/null +++ b/plans/admin-redesign/phase-5-quick-actions.md @@ -0,0 +1,94 @@ +# Phase 5: Per-row quick actions + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** frontend +**Parallelizable with:** Phase 3, Phase 4 + +## Objective + +На hover-строке user / session показывать кнопки частых action прямо в таблице — без открытия overlay-панели. Сокращает 2-3 клика до 1 для типичных задач (бан, выдача монет, удаление сессии). + +## Tasks + +- [ ] **Users table** (`frontend/js/admin/sections/users.js`): + - Добавить в каждый `` дополнительную ячейку или абсолютно-позиционированный блок с action-кнопками + - Visible: только на `:hover` строки (via CSS) + - Кнопки: + - **🔒 Ban / Unban** — открывает confirm modal, на confirm вызывает существующий `toggleBanUser()` (или его эквивалент с userId) + - **🪙 Award coins** — открывает быстрый prompt-modal "Сколько монет?", вызывает существующий `shopAdminAwardCoins` без перехода в shop tab + - **📜 Sessions** — навигирует через `AdminRouter.navigate('#sessions?user=' + uid)` (param Phase 6 будет обрабатывать; пока fallback — переход на sessions tab) + - **🗑 Delete** — confirm, вызывает существующий `confirmDeleteUser` + - **ВАЖНО:** иконки только inline SVG (.ic класс) или Lucide — НИКАКИХ эмоджи + - Кнопки `event.stopPropagation()` чтобы не триггерить `openUserPanel` +- [ ] **Sessions table** (`frontend/js/admin/sections/sessions.js`): + - **👁 View** — открыть session detail (текущий механизм) + - **🗑 Delete** — confirm + DELETE /admin/sessions/:id (если такой endpoint есть, иначе добавить) +- [ ] **Если delete session endpoint отсутствует** — добавить в backend: + - `DELETE /api/admin/sessions/:id` с auth admin only + - Контроллер: удалить из `test_sessions` + connected `session_answers` + - Audit log entry +- [ ] **CSS** (в admin.html style блоке или новый файл): + ```css + .row-actions { opacity: 0; transition: opacity .15s; display: inline-flex; gap: 4px; } + tr:hover .row-actions { opacity: 1; } + .row-action-btn { width: 28px; height: 28px; border-radius: 6px; ... } + ``` +- [ ] Подсказки через `title="..."` атрибут на каждой кнопке +- [ ] Confirm-модалки используют `LS.confirm` (не reinventing) + +## Files to Modify/Create + +- `frontend/js/admin/sections/users.js` — модификация renderRow + action handlers (~50-100L добавления) +- `frontend/js/admin/sections/sessions.js` — same (~30-50L) +- `frontend/admin.html` — стили для `.row-actions` (~30L) +- `backend/src/controllers/adminController.js` — `deleteSession` если отсутствует +- `backend/src/routes/admin.js` — `DELETE /sessions/:id` если отсутствует + +## Acceptance Criteria + +- Hover на user row → видны 4 кнопки справа без раздвигания layout +- Hover на session row → видны 2 кнопки +- Каждая кнопка работает (ban / coins / sessions / delete) +- Click на кнопку НЕ открывает user-panel overlay (stopPropagation) +- Tooltip на hover каждой кнопки +- Confirm для деструктивных action (delete, ban) +- LS.toast после success +- Auth check — все action available только admin +- Mobile: actions hidden (tap-only context), либо альтернативный UI (long-press → menu) — пока минимум скрыть на ≤768px + +## Notes + +### Существующие helpers использовать + +- `LS.confirm(message, { okText, danger })` для подтверждений +- `LS.modal(...)` если нужна форма (например award coins amount) +- `LS.toast` для feedback +- Существующие admin* функции (toggleBanUser, awardCoins, etc.) — не дублировать + +### Визуальный паттерн + +Inspired by Linear / Vercel admin: actions visible on row hover, positioned right-aligned, ghost-style buttons (transparent bg, border on hover). Иконки только. + +### Что НЕ делать в этой фазе + +- Не делать bulk-actions (select multiple → action) — это после merge +- Не делать undo (toast с "отменить" внутри) — Phase 6+ +- Не менять структуру таблицы radically + +## Review Checklist + +- [ ] Кнопки не сдвигают layout (используют absolute / hidden / opacity) +- [ ] Все action эскейпят пользовательский ввод +- [ ] No emoji — только SVG +- [ ] event.stopPropagation на всех кнопках +- [ ] Confirm для destructive actions +- [ ] Tooltip присутствует +- [ ] Mobile-friendly (hidden или альтернативный UI) +- [ ] Build passes + +## Handoff to Next Phase + + diff --git a/plans/admin-redesign/phase-6-deep-pages.md b/plans/admin-redesign/phase-6-deep-pages.md new file mode 100644 index 0000000..6341146 --- /dev/null +++ b/plans/admin-redesign/phase-6-deep-pages.md @@ -0,0 +1,117 @@ +# Phase 6: Deep entity pages + +**Status:** ⬜ Not Started +**Parent plan:** [PLAN.md](./PLAN.md) +**Domain:** frontend + +## Objective + +Заменить выезжающую `.user-panel` overlay на полноценную страницу с URL `#users/123`. Аналогично для session: `#sessions/456` = full detail page. Это самая комплексная фаза — она ломает совместимость с старым overlay UI (удаляет код), потому идёт ПОСЛЕ всех остальных. + +## Tasks + +- [ ] **User detail page** (`frontend/js/admin/sections/user-detail.js`): + - Реагирует на route `#users/:id` + - Layout: + - **Header**: avatar, name, role badge, email, action buttons (ban/edit/perms/delete), back-link to `#users` + - **Tabs** (sub-nav в странице): + - Overview — статистика (тестов, средний %, регистрация, посл вход) + - Sessions — таблица последних 20 сессий с pagination + - Classes — список классов где он состоит + - Audit — журнал действий (если есть audit log с user_id) + - **Graphs** (опционально, можно отдельным таб'ом): + - Простой SVG-чарт: успеваемость по неделям + - Mini-bar chart: avg % по предметам +- [ ] **Session detail page** (`frontend/js/admin/sections/session-detail.js`): + - Реагирует на route `#sessions/:id` + - Layout: header (user, subject, score, дата) + список вопросов/ответов (правильно/нет, текст), back-link +- [ ] **Router updates** (`frontend/js/admin/router.js` если ещё не поддерживает): + - `#users/123` → emit { route: 'users', params: ['123'] } + - `#sessions/456` → emit { route: 'sessions', params: ['456'] } +- [ ] **Admin.js dispatch**: + - При route с params → init detail-section вместо list-section + - При route без params → init list-section (как раньше) +- [ ] **Удалить overlay-код:** + - В `frontend/admin.html` удалить `
` блок + - В `sections/users.js` удалить `openUserPanel`, `closeUserPanel`, `reloadUserPanel` + - В `sections/users.js` поменять onclick: `onclick="openUserPanel(event,${u.id},'${u.role}')"` → `onclick="AdminRouter.navigate('#users/${u.id}')"` +- [ ] **Replace** в Phase 5 quick action "Sessions" — теперь `AdminRouter.navigate('#users/${uid}/sessions')`: + - Парсить sub-tab из route + - Открывать user-detail page с активным Sessions tab +- [ ] **Глоссарий routes после фазы:** + - `#overview` — dashboard (Phase 3) + - `#users` — list + - `#users/123` — user detail (overview tab default) + - `#users/123/sessions` — user detail with sessions sub-tab + - `#sessions` — list + - `#sessions/456` — session detail + - … остальные без params — как было + +## Files to Modify/Create + +- `frontend/js/admin/sections/user-detail.js` — новый, ~400-600L +- `frontend/js/admin/sections/session-detail.js` — новый, ~200-300L +- `frontend/admin.html` — удалить `.user-panel` overlay, добавить `
` и `
`, добавить `