# Phase 4: Cmd+K command palette

**Status:** ✅ Done
**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

- [x] 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)
- [x] 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
- [x] 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
- [x] Открытие результата:
  - 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
- [x] Стили palette: глассморфизм/blur, центрировано, max-width 600px, dark/light theme-friendly. Использовать существующие токены `--surface`, `--border`, `--text-2`.
- [x] Подсказка в 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

### Endpoint contract — `GET /api/admin/search?q=<query>&limit=8`

Auth: admin only (inside `requireRole('admin')` block in `backend/src/routes/admin.js`).
If `q.trim().length < 2`, returns empty arrays without hitting DB. Errors → 500 `{error:'Search failed'}`.

Response shape (top 5 users / top 3 tests / top 3 classes):
```js
{
  users:   [{ id, name, email, role }],
  tests:   [{ id, name, subject_slug }],     // alias: tests.title AS name
  classes: [{ id, name, code }],              // alias: classes.invite_code AS code
}
```

Backend perf: 3 simple parameterised SELECTs with LIMIT — well under 100ms.

### Navigation contract from palette → router

| Result kind | Action |
|-------------|--------|
| Action      | calls the hardcoded `go()` callback (most go through `AdminRouter.navigate('#…')`) |
| User        | `AdminRouter.navigate('#users/' + id)` — params parsed by router, but ROUTE_TO_SECTION currently only dispatches `users` section. **Phase 6** can add a `user` section that reads `params.id` and renders a deep page. |
| Test        | `AdminRouter.navigate('#tests')` (no deep page yet) |
| Class       | `window.location.href = '/classes#' + id` — leaves admin (classes UI is a separate page) |

### Action registry (hardcoded in `frontend/js/admin/palette.js`)

`award_coins → #shop`, `award_xp → #gam`, `new_class → /classes`, `new_test → #tests`,
`view_users → #users`, `view_sessions → #sessions`, `view_audit → #sublog`, `view_overview → #overview`.

Phase 5 (quick actions) and Phase 6 (deep pages) may extend the `ACTIONS` array — just add to it; the action's `name` field is what users see and what is fuzzy-matched (lowercase substring on `name` + optional `hint` keyword).

### Ctrl+K conflict with `/js/search.js`

`/js/search.js` is also loaded on admin.html and binds its own Ctrl+K listener (bubble phase). Palette binds in **capture phase** + `e.stopImmediatePropagation()`, so on admin pages the palette wins. On non-admin pages the generic search remains intact (palette.js is only loaded from admin.html).

### Exposed globals

- `window.AdminPalette = { open, close, isOpen }` — for future programmatic open from quick-actions.
- `LS.adminGlobalSearch(q)` — exported helper in `js/api.js`.
