# 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` — добавить `` ## 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