chore(plan): admin-redesign 6-phase plan

PLAN.md + 6 subplans + CONTEXT.md

Strategy: Incremental | Mode: Automated | Execution: Orchestrator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-16 21:47:55 +03:00
parent bd7a9dbee2
commit 76e376ee04
8 changed files with 831 additions and 0 deletions
+70
View File
@@ -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 должны работать
- Старые ссылки `<a href="#stats">` (если есть) тоже
### Конвенции вновь создаваемых модулей
Каждая section (фаза 2):
```js
// js/admin/sections/<name>.js
(function () {
'use strict';
let inited = false;
async function load() { /* ... */ }
window.AdminSections = window.AdminSections || {};
window.AdminSections.<name> = {
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.
+84
View File
@@ -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 `<script>`)
- 13 табов: stats, questions, tests, assignments, subjects, users, sessions, permissions, shop, gam, tpl, sims, games, sublog
- `switchTab()` ручной tab-роутер, состояние теряется при F5
- User detail = выезжающая `.user-panel` overlay внутри tab-users
**Цели:**
- F5/bookmark на `#users/123` работают
- admin.js ≤ 800L
- Dashboard + Ctrl+K + hover-actions для частых сценариев
- Полноценная страница user/session вместо overlay
## Build & Test Commands
- **Start:** `cd backend && npm start` (vanilla JS, нет бандлера — server раздаёт static)
- **Dev:** `cd backend && npm run dev` (nodemon)
- **Test:** `cd backend && npm test` (node --test)
- **Lint:** `cd backend && npm run lint:routes` (route auth checker)
- **Manual verify:** открыть `http://localhost:3000/admin` и пройти основные сценарии
## Phases
- [ ] Phase 1: Hash-router [domain: frontend] → [subplan](./phase-1-hash-router.md)
- [ ] Phase 2: Split admin.html → per-section modules [domain: frontend] → [subplan](./phase-2-split-sections.md)
- [ ] Phase 3: Dashboard #overview [domain: fullstack] → [subplan](./phase-3-dashboard.md) (parallelizable with 4, 5)
- [ ] Phase 4: Cmd+K command palette [domain: fullstack] → [subplan](./phase-4-palette.md) (parallelizable with 3, 5)
- [ ] Phase 5: Per-row quick actions [domain: frontend] → [subplan](./phase-5-quick-actions.md) (parallelizable with 3, 4)
- [ ] Phase 6: Deep entity pages [domain: frontend] → [subplan](./phase-6-deep-pages.md)
**Параллелизация:** фазы 3, 4, 5 независимы (touch different files, no shared state) — выполняются параллельно после завершения фазы 2.
## Phase Progress Log
| Phase | Domain | Status | Review | Build | Committed |
|-------|--------|--------|--------|-------|-----------|
| Phase 1: Hash-router | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 2: Split sections | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 3: Dashboard | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 4: Palette | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 5: Quick actions | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 6: Deep pages | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
## Final Review
- [ ] Comprehensive code review (final-reviewer agent)
- [ ] Security review (auth-touching changes in new endpoints)
- [ ] Full build passes (server starts, no errors)
- [ ] Manual smoke test
- [ ] Merged to `master`
## Acceptance Criteria (whole feature)
- F5 на любом `#sub-route` восстанавливает state
- admin.js ≤ 800L
- Ctrl+K находит пользователя по имени за <100ms
- Dashboard `#overview` показывает данные за 24ч
- Per-row hover-actions на users/sessions
- `#users/123` = полноценная страница, не overlay
- Все existing onclick handlers продолжают работать (backward compat в фазах 1-5)
- Нет регрессий в тестах
## Tech Stack & Conventions Reference
- **Stack:** vanilla JS, Express 4, SQLite (better-sqlite3 sync), JWT, WebSocket+SSE, KaTeX, Lucide
- **Frontend:** pages = `frontend/*.html`, JS = `/js/*` или `frontend/js/*`, все API через `window.LS.*`
- **UI primitives:** `LS.modal`, `LS.confirm`, `LS.toast`, `LS.state`, `LS.skeleton`, `LS.esc`
- **localStorage prefix:** `ls_*`
- **Icons:** inline SVG `.ic` или Lucide CDN — **эмоджи запрещены**
- **Search в коде:** только ast-index (пользователь категорически запретил Grep)
- **Backend:** layer-based — `controllers/`, `routes/`, `services/`, `db/migrations/NNN_*.sql`
@@ -0,0 +1,90 @@
# Phase 1: Hash-router
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Заложить фундамент для URL-роутинга admin-панели через `location.hash`. После этой фазы можно делать F5 на `#users`, делиться deep-links, использовать browser back/forward. Старая система табов (`switchTab`) продолжает работать без изменений — router её обёртывает, а не заменяет.
## Tasks
- [ ] Создать `frontend/js/admin/router.js` с `window.AdminRouter`:
- `parse(hash)``{ route: 'users', params: ['123'], raw }`
- `navigate(routeOrHash, { replace?, silent? })` — программная навигация
- `current()` → текущий route object
- `on(event, fn)` / `off(event, fn)` — pub/sub для 'change' event
- Поддержка форматов: `#stats`, `#users`, `#users/123`, `#sessions/456`
- [ ] Подключить `router.js` в `admin.html` ДО `admin.js`
- [ ] В `admin.js` модифицировать `switchTab(btn)`:
- Дополнительно вызывать `AdminRouter.navigate('#' + name, { silent: true })`
- НЕ удалять старую логику
- [ ] Добавить листенер `AdminRouter.on('change', ...)` в admin.js:
- При route change → найти соответствующий `.admin-nav-item[data-tab="X"]` и активировать его (через имеющийся switchTab, но с `silent`-флагом чтобы избежать рекурсии)
- [ ] При инициализации страницы:
- Если `location.hash` пустой → set default `#stats`
- Если есть hash → распарсить и переключить на соответствующий tab
- [ ] Логировать unknown routes: `console.warn('AdminRouter: unknown route', route)` + fallback на `#stats`
- [ ] Защита от инфинит-loop'а: флаг `_routerNavigating` при programmatic-навигации, чтобы handler не реагировал на свой же hash change
## Files to Modify/Create
- `frontend/js/admin/router.js` — новый, ~80-120L
- `frontend/admin.html` — добавить `<script src="/js/admin/router.js"></script>` в `<head>` или перед 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
<!-- Заполнит implementer после фазы.
Должно содержать: где живёт router API, какие события эмитятся, как Phase 2 sections подписываются на route changes. -->
@@ -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/<name>.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.<name> = {
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 `<script>` тегов добавить в `admin.html` (после router.js, перед admin.js)
- [ ] Глобальные функции, которые используются из onclick HTML, остаются доступными через `window.X`:
- `changeRole`, `openUserPanel`, `goAddQuestion`, `confirmDeleteUser`, etc.
- Каждый section module экспортирует свои onclick-handler'ы через `window.X = X` или через делегацию из admin.js
- [ ] Удалить per-tab `*Inited` флаги из admin.js — они переехали внутрь section modules
## Files to Modify/Create
- `frontend/js/admin/sections/stats.js` — новый
- `frontend/js/admin/sections/users.js` — новый, ~400-500L
- `frontend/js/admin/sections/sessions.js` — новый
- `frontend/js/admin/sections/questions.js` — новый, ~800L
- `frontend/js/admin/sections/tests.js` — новый
- `frontend/js/admin/sections/assignments.js` — новый
- `frontend/js/admin/sections/subjects.js` — новый
- `frontend/js/admin/sections/permissions.js` — новый
- `frontend/js/admin/sections/shop.js` — новый
- `frontend/js/admin/sections/gam.js` — новый
- `frontend/js/admin/sections/tpl.js` — новый
- `frontend/js/admin/sections/sims.js` — новый
- `frontend/js/admin/sections/games.js` — новый
- `frontend/js/admin/sections/sublog.js` — новый
- `frontend/js/admin/admin.js` — сильно ужать (с 3500L до ~500-800L)
- `frontend/admin.html` — добавить 13 `<script>` тегов
## Acceptance Criteria
- `admin.js` ≤ 800L (от 3500L)
- Каждый section-файл ≤ 900L (questions.js самый большой)
- Все 13 табов работают идентично текущему поведению (no regressions)
- Cross-tab handlers (`goAddQuestion`, `confirmDelete*`) работают
- Lazy-load работает: при первом открытии tab делается fetch, при повторном — нет
- F5 на любом `#X` корректно ленево-грузит секцию (через router из Phase 1)
- Browser back/forward работают
- Никаких console errors в Devtools
## Notes
### Стратегия извлечения
Один section за раз, мелкими безопасными шагами:
1. Скопировать функции `loadX, openXModal, deleteX, ...` в новый файл sections/<name>.js, обернуть в IIFE
2. Экспортировать через `window.AdminSections.X`
3. Подключить `<script>` в admin.html
4. В admin.js заменить вызовы (`loadX()` → `AdminSections.X.init(ctx)`)
5. Удалить дубликаты в admin.js
6. Тест: открыть tab — работает?
7. Перейти к следующей секции
### Что НЕ переезжает в sections
- `LS.initPage()` + auth check — остаётся в admin.js
- `switchTab` (helper) — остаётся
- `pctClass`, `fmtDate`, `fmtTime` — общие утилиты, остаются (или переезжают в `admin/_shared.js`)
- Sidebar / notif init — остаётся
- Router setup — остаётся
### Глобальные функции из onclick
Сейчас многие функции вызываются из HTML onclick (`onclick="openUserPanel(...)"`). Чтобы не переписывать HTML на этой фазе, в каждом section module экспортируем нужные функции через `window.X = X` внутри IIFE. Phase 5/6 могут заменить onclick на event delegation, но Phase 2 этого не делает (incremental).
### Тестирование каждой секции
После каждой выделенной секции:
- Открыть `/admin` → переключиться на этот tab → данные загрузились
- Все кнопки/модалки секции работают
- Cross-tab navigation (если есть) работает
- F5 на `#<route>` корректно открывает tab
Если регрессия — откатить эту итерацию, разобраться, починить.
### Совет implementer'у
Если фаза становится огромной — можно сделать несколько коммитов внутри phase branch. Это inscope. Не нужно делать один гигантский коммит на 14 файлов.
## Review Checklist
- [ ] Все 13 секций имеют одинаковую структуру (init/reload)
- [ ] admin.js ≤ 800L, в нём нет дублирования с sections
- [ ] Все window.X экспорты есть для onclick handlers
- [ ] Lazy-init работает (профилировка: при открытии tab → fetch, при повторе → нет)
- [ ] F5 на каждом из 13 routes восстанавливает секцию
- [ ] Build passes: server starts, no errors
## Handoff to Next Phase
<!-- Implementer должен зафиксировать:
- Структуру window.AdminSections.X.init/reload (точное API)
- Какие функции стали глобальными через window.X (список)
- Как Phase 3 (dashboard) должна добавиться: новый sections/overview.js + new route в ROUTE_TO_SECTION
- Где живут shared утилиты (pctClass, fmtDate, esc) — admin.js или вынесены в _shared.js -->
+112
View File
@@ -0,0 +1,112 @@
# Phase 3: Dashboard #overview
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
**Parallelizable with:** Phase 4, Phase 5
## Objective
Сделать `#overview` дефолтным route'ом admin-панели — landing-страница "что требует внимания". Заменяет нынешние обезличенные stat-cards (totalUsers, totalTests, ...) на actionable digest за последние 24-48 часов.
## Tasks
- [ ] Backend: новый endpoint `GET /api/admin/overview`:
```js
{
newUsers24h: number, // регистрации за 24ч
newSessions24h: number, // запущенных тестов
bannedThisWeek: [{ id, name, banned_at }],
failedSessions24h: number, // тесты со статусом не completed
topSessions24h: [{ id, user_name, subject, score, total, finished_at }], // топ-5 за 24ч
activeUsers24h: number, // unique last_login за 24ч
pendingMigrations: number, // если возможно проверить
recentErrors: [{ id, type, message, created_at }] // если есть audit log с типом 'error'
}
```
- Контроллер: `backend/src/controllers/adminController.js` → новая функция `getOverview`
- Route: добавить в `backend/src/routes/admin.js`
- Auth: admin или teacher (как остальные admin/* — RBAC same)
- Performance: один запрос для каждого поля, простые COUNT/SELECT, без JOIN'ов где возможно
- [ ] Frontend: новый section `frontend/js/admin/sections/overview.js`:
- Использует структуру из Phase 2
- Загружает `/api/admin/overview`
- Рендерит карточки в `<div id="tab-overview">` секции:
- **Активность 24ч** — registr, sessions, active users (число + спарклайн если есть данные)
- **Требует внимания** — banned this week (если >0), failed sessions (если >5%), pending migrations
- **Топ-сессии** — таблица top-5 за день с click→drilldown
- **Quick links** — "Все пользователи", "Все сессии", "Создать тест" (deep-link в соответствующие routes)
- LS.skeleton при загрузке, LS.state.error на fail
- [ ] HTML: добавить `<div class="tab-pane" id="tab-overview">` в admin.html (перед остальными tab-pane)
- [ ] Nav: добавить admin-nav-item для `overview` (icon: layout-dashboard / activity)
- [ ] Регистрация в ROUTE_TO_SECTION (из Phase 2): `overview: 'overview'`
- [ ] Сделать `#overview` дефолтным route'ом в router (из Phase 1) — если пустой hash, navigate to `#overview` вместо `#stats`
- [ ] Старый `#stats` остаётся как доступный route (legacy backend stats), но не дефолтный
## Files to Modify/Create
- `backend/src/controllers/adminController.js` — добавить `getOverview` функцию (~60-90L)
- `backend/src/routes/admin.js` — добавить `router.get('/overview', requireAdmin, getOverview)`
- `frontend/js/admin/sections/overview.js` — новый, ~250-350L
- `frontend/admin.html` — добавить `<div id="tab-overview">` + nav-item + `<script>` тег
- `frontend/js/admin/admin.js` — изменить default route на `#overview` (если ещё не сделано через router.js config)
## Acceptance Criteria
- `/admin` (без hash) → редирект на `#overview`
- `#overview` показывает реальные данные (новые регистрации видны если кто-то зарегистрировался)
- Карточки кликабельные (click → deep-link в users / sessions с фильтром)
- При отсутствии данных (свежая БД) — empty states, не crash
- Endpoint выполняется <100ms на тестовой БД
- F5 на `#overview` работает
- Auth: только teacher/admin (как остальные /admin/*)
## Notes
### Что считать "требует внимания"
Делаем простые threshold'ы для MVP:
- `bannedThisWeek` > 0 — показать жёлтую карточку с именами
- `failedSessions24h` > 0 — показать список (failed = status != 'completed')
- `pendingMigrations` > 0 — показать красную (но это редкий случай — миграции применяются на старте)
Можно потом расширить до "новых жалоб", "переполненных классов", и т.д. — это уже после merge.
### Дизайн карточек
Bento-grid из 4-6 карточек:
```
+---------+---------+
| 24ч | Внимание|
| метрики | ! |
+---------+---------+
| Топ сессии |
| (table) |
+---------+---------+
| Quick links |
+-------------------+
```
Использовать существующие стили `.stat-card`, `.section-title` из admin.html — не изобретать.
### Что НЕ делать в этой фазе
- Не делать реалтайм WebSocket-обновления (это уже Phase 7+)
- Не делать графики/чарты (пока числа + sparkline опционально)
- Не делать персонализацию (например "ваши классы")
## Review Checklist
- [ ] Endpoint работает и возвращает корректную форму ответа
- [ ] Frontend handles empty state gracefully
- [ ] Click на quick-link корректно навигирует через AdminRouter
- [ ] Нет hardcoded date math (использовать SQL `datetime('now', '-24 hours')`)
- [ ] Roles correct (admin/teacher only, не у students)
- [ ] No SQL injection — параметры через `?` placeholders
- [ ] Build passes
## Handoff to Next Phase
<!-- Implementer: записать, какой shape у overview-ответа,
какие route переходы внедрены, какие deep-links открыты для Phase 6
(например, `#users?filter=banned` или `#sessions?status=failed`). -->
+113
View File
@@ -0,0 +1,113 @@
# Phase 4: Cmd+K command palette
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
**Parallelizable with:** Phase 3, Phase 5
## Objective
Глобальный палеттный поиск по Ctrl+K (Cmd+K на Mac) — нахоит entities (users, tests, classes, sessions) + actions ("выдать монеты ученику", "разбанить", "создать класс", deep-link routes). Радикально сокращает количество кликов для частых сценариев.
## Tasks
- [ ] Backend: новый endpoint `GET /api/admin/search?q=X&limit=8`:
- Возвращает смешанный результат:
```js
{
users: [{ id, name, email, role }], // top 5 по name LIKE / email LIKE
tests: [{ id, name, subject_slug }], // top 3
classes: [{ id, name, code }], // top 3
sessions: [] // skip пока, добавим если нужно
}
```
- Контроллер: новая функция `globalSearch` в `adminController.js`
- Route: `router.get('/search', requireAdmin, globalSearch)`
- Каждая sub-query SELECT отдельно с LIMIT, общий ответ — простой json
- Auth: admin only (teachers видят только своих учеников; для упрощения — admin)
- [ ] Frontend: `frontend/js/admin/palette.js` — palette модуль:
- Не section, а глобальный widget — подключается в admin.js init
- Слушает `keydown` на `Ctrl+K` / `Cmd+K` (preventDefault)
- Открывает modal через `LS.modal()`:
- Header: search input (autofocus)
- Body: список результатов с keyboard nav (↑↓ Enter Esc)
- Иконка типа справа от каждого результата (User, Test, Class, Action)
- Дебаунс поиска ~150ms
- Min длина query: 2 символа
- При query='' → показать "Recent Actions" hardcoded list
- [ ] Actions index (hardcoded в palette.js):
```js
const ACTIONS = [
{ id: 'award_coins', name: 'Выдать монеты', icon: 'coins', handler: () => AdminRouter.navigate('#shop') },
{ id: 'award_xp', name: 'Выдать XP', icon: 'zap', handler: () => AdminRouter.navigate('#gam') },
{ id: 'new_class', name: 'Создать класс', icon: 'plus-circle', handler: () => window.location.href = '/classes' },
{ id: 'new_test', name: 'Создать тест', icon: 'file-plus', handler: () => AdminRouter.navigate('#tests') },
{ id: 'view_users', name: 'Все пользователи', icon: 'users', handler: () => AdminRouter.navigate('#users') },
{ id: 'view_sessions', name: 'Все сессии', icon: 'history', handler: () => AdminRouter.navigate('#sessions') },
{ id: 'view_audit', name: 'Audit log', icon: 'shield', handler: () => AdminRouter.navigate('#sublog') },
// …добавлять по мере надобности
];
```
- Fuzzy-match в JS (substring match по name) при query
- [ ] Открытие результата:
- User → `AdminRouter.navigate('#users/' + id)` (Phase 6 будет рендерить deep page; пока fallback на `#users` + opening user-panel через имеющийся `openUserPanel`)
- Test → `AdminRouter.navigate('#tests')` + scroll к row (если поддерживается, иначе просто tab)
- Class → `window.location.href = '/classes#' + id`
- Action → выполнить handler
- [ ] Стили palette: глассморфизм/blur, центрировано, max-width 600px, dark/light theme-friendly. Использовать существующие токены `--surface`, `--border`, `--text-2`.
- [ ] Подсказка в UI: footer dialog'а "↑↓ — навигация · ↵ — выбрать · esc — закрыть"
## Files to Modify/Create
- `backend/src/controllers/adminController.js` — добавить `globalSearch` (~60L)
- `backend/src/routes/admin.js` — добавить `/search` route
- `js/api.js` — добавить `LS.adminGlobalSearch(q)` helper (~5L)
- `frontend/js/admin/palette.js` — новый, ~300-400L
- `frontend/admin.html` — добавить `<script src="/js/admin/palette.js"></script>`
## 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
<!-- Implementer: записать, какой формат ответа /admin/search,
как palette вызывает navigate (важно для Phase 6 — deep user page будет ловить #users/N),
какие actions zarejestrowano (Phase 6 может добавить ещё). -->
@@ -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`):
- Добавить в каждый `<tr>` дополнительную ячейку или абсолютно-позиционированный блок с 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
<!-- Implementer: записать, какие action-кнопки добавлены,
какие param-форматы router использует (`#sessions?user=N`),
что Phase 6 deep-page должна включить (например, replace #users overlay на deep page). -->
+117
View File
@@ -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` удалить `<div class="user-panel" id="user-panel">` блок
- В `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, добавить `<div id="tab-user-detail">` и `<div id="tab-session-detail">`, добавить `<script>` теги
- `frontend/js/admin/sections/users.js` — удалить overlay-функции (~100-150L удаления)
- `frontend/js/admin/router.js` — улучшения parsing для sub-routes (если нужно)
- `frontend/js/admin/admin.js` — dispatch logic для routes с params
## Acceptance Criteria
- Click на user row → URL становится `#users/123`, открывается deep page
- F5 на `#users/123` восстанавливает страницу
- Back navigation → возврат на `#users` list
- Header содержит все action-кнопки (ban, edit, perms, delete)
- Sub-tabs (overview, sessions, classes, audit) переключаются, URL обновляется
- Старая `.user-panel` overlay полностью удалена из HTML и JS
- Click на session id (в любом контексте) → `#sessions/456` → detail page
- Нет console errors
- Графики (если делаются) рендерятся корректно
## Notes
### Backward compat
После Phase 6 старые ссылки/onclick типа `openUserPanel(...)` УЖЕ НЕ работают. Это intentional — мы их удалили. Но `onclick="AdminRouter.navigate('#users/N')"` работает везде.
Если есть external links на админку user-panel — они продолжат работать как `#users/N` через router.
### Графики
Можно использовать chart.js (CDN ~50KB), но проще — inline SVG bar/line chart на нескольких десятках строк. У нас уже есть `.perf-bar` для процентов — можно расширить.
Не обязательно делать графики в этой фазе — можно сделать MVP без них и добавить чартами позже. В acceptance criteria графики помечены опционально.
### Audit log
Если в БД есть таблица `audit_log` с `user_id` — sub-tab Audit показывает её. Если нет — sub-tab скрывается или показывает empty state "Audit logging не активирован".
### Session detail
Сейчас session detail открывается через `adminGetSessionDetail` → возвращает массив answers. Используем тот же endpoint, рендерим в полноценную страницу вместо modal.
### Удаление overlay-кода (опасный шаг)
Делать в КОНЦЕ фазы, после того как deep page работает. Сначала добавить deep page, протестировать, потом удалить overlay. Можно даже сделать отдельным коммитом ("remove overlay").
### Что НЕ делать
- Не делать realtime updates (Phase 7+)
- Не делать collaborative cursors
- Не оптимизировать графики до production-grade (chart.js or similar OK)
## Review Checklist
- [ ] Deep pages работают: F5, back/forward
- [ ] Sub-tabs URL-обновляемы
- [ ] Old overlay code fully removed
- [ ] No regressions: ban/edit/delete user работают из deep page
- [ ] Mobile-friendly: tabs scrollable, layout не ломается
- [ ] Build passes
- [ ] **Final smoke test:** пройти полный сценарий — открыть админку, найти пользователя через Cmd+K, перейти на deep page, выдать монеты, посмотреть сессии, забанить, разбанить, вернуться в overview
## Handoff to Next Phase
<!-- Это финальная фаза. Implementer записывает: что ещё не сделано,
какие follow-up задачи стоит зафиксировать (графики, realtime, мобильная версия).
Эти заметки помогут final-reviewer и при подготовке merge-summary. -->