76e376ee04
PLAN.md + 6 subplans + CONTEXT.md Strategy: Incremental | Mode: Automated | Execution: Orchestrator Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
6.8 KiB
Markdown
114 lines
6.8 KiB
Markdown
# 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 может добавить ещё). -->
|