feat(admin): phase 4 — Cmd+K command palette
Global search modal: actions + users + tests + classes.
- GET /api/admin/search?q=X (~50L controller): 3 parameterized LIKE queries, admin-only
- frontend/js/admin/palette.js (~366L): custom lightweight modal (not LS.modal — footer-button oriented), Ctrl+K/Cmd+K capture-phase override of generic /js/search.js, debounce 150ms, race-guard via _reqSeq, min-query 2 chars, 8 hardcoded actions, ↑↓ wrap + Enter, click-outside close
- adminGlobalSearch helper: drop ignored 'limit' param (server hardcodes 5/3/3)
window.AdminPalette = { open, close, isOpen } exposed for Phase 5/6 use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
- ✅ Phase 1 implemented — `window.AdminRouter` обёртывает старый `switchTab` (hash ↔ tab двусторонне). `switchTab` принимает 2-й аргумент `{ fromRouter: true }` для предотвращения рекурсии. Default = `#stats`. Файлы: `frontend/js/admin/router.js` (новый), `frontend/admin.html` (+1 строка), `frontend/js/admin/admin.js` (модификация `switchTab` + IIFE `initAdminRouter`).
|
||||
- ✅ Phase 2 implemented (commit 92030b4) — admin.js ужат с ~3591L до 701L. Все 13 plan-tabs живут в `frontend/js/admin/sections/*.js` (IIFE pattern) + `frontend/js/admin/_shared.js` (window.AdminCtx). switchTab() диспетчит в `AdminSections[ROUTE_TO_SECTION[name]].init()`. Lazy-load работает (inited флаг внутри каждой IIFE). System tabs (topics/audit/errors/health/classroom/avatars) остались inline в admin.js — Phase 2 их не extract'ил.
|
||||
- ✅ Phase 3 implemented — `#overview` стал дефолтным route'ом admin-панели. Backend: `GET /api/admin/overview` (admin-only, ~0.08ms/call) возвращает digest за 24ч: новые регистрации, запущенные сессии, активные юзеры, активные классы, failed-сессии, забаненные за неделю (из audit log), топ-5 завершённых сессий. Frontend: `frontend/js/admin/sections/overview.js` (~205L) рендерит bento-grid карточки + alerts + топ-таблицу + quick-links (deep-link через `AdminRouter.navigate`). `admin.js`: дефолт `'stats'` → `'overview'` в `activate()`, initial nav, и initial init. Old `#stats` остался работающим (доступен через nav-item). Файлы: `frontend/js/admin/sections/overview.js` (NEW), `backend/src/controllers/adminController.js` (+57L: `overviewStmts` + `getOverview`), `backend/src/routes/admin.js` (+1L), `js/api.js` (+1 helper), `frontend/admin.html` (nav-item + tab-pane + script tag), `frontend/js/admin/admin.js` (ROUTE_TO_SECTION + default route refs).
|
||||
- ⬜ Phase 4-6 not started
|
||||
- ✅ Phase 4 implemented — Cmd+K (Ctrl+K) global command palette. Backend: `GET /api/admin/search?q=X` (admin-only) returns `{users[5], tests[3], classes[3]}` via 3 prepared LIKE queries (`title AS name` for tests, `invite_code AS code` for classes). Frontend: `frontend/js/admin/palette.js` (~320L) — custom modal (NOT LS.modal) with capture-phase Ctrl+K listener that `stopImmediatePropagation`'s to override `/js/search.js`. Debounced 150ms, ↑↓ Enter Esc keyboard nav, click-outside close. Action registry (8 entries) is hardcoded — extend by appending to `ACTIONS` const. Result interactions: user → `AdminRouter.navigate('#users/' + id)` (Phase 6 deep page hook), test → `#tests`, class → `/classes#id`. Exposed: `window.AdminPalette = { open, close, isOpen }`, `LS.adminGlobalSearch(q)`. Files: `frontend/js/admin/palette.js` (NEW), `backend/src/controllers/adminController.js` (+50L: `searchStmts` + `globalSearch`), `backend/src/routes/admin.js` (+1L), `js/api.js` (+4L helper + export), `frontend/admin.html` (+1 script tag).
|
||||
- ⬜ Phase 5-6 not started
|
||||
|
||||
## Temporary Workarounds
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
- [x] Phase 1: Hash-router [domain: frontend] → [subplan](./phase-1-hash-router.md)
|
||||
- [x] Phase 2: Split admin.html → per-section modules [domain: frontend] → [subplan](./phase-2-split-sections.md)
|
||||
- [x] 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)
|
||||
- [x] 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)
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
|-------|--------|--------|--------|-------|-----------|
|
||||
| Phase 1: Hash-router | frontend | ✅ Done | ✅ PASS w/ notes | ✅ | ✅ 8a7bed4 |
|
||||
| Phase 2: Split sections | frontend | ✅ Done | ✅ PASS (1 blocker fixed: fa67ad1) | ✅ node --check | ✅ 92030b4 + fa67ad1 |
|
||||
| Phase 3: Dashboard | fullstack | ✅ Done | ⬜ pending | ✅ node --check + queries verified | ⬜ |
|
||||
| Phase 4: Palette | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 3: Dashboard | fullstack | ✅ Done | ✅ PASS w/ 3 SQL warnings (post-merge polish) | ✅ | ✅ 41acbdd |
|
||||
| Phase 4: Palette | fullstack | ✅ Done | ⬜ | ✅ node --check | ⬜ |
|
||||
| Phase 5: Quick actions | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 6: Deep pages | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Phase 4: Cmd+K command palette
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Status:** ✅ Done
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** fullstack
|
||||
**Parallelizable with:** Phase 3, Phase 5
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Backend: новый endpoint `GET /api/admin/search?q=X&limit=8`:
|
||||
- [x] Backend: новый endpoint `GET /api/admin/search?q=X&limit=8`:
|
||||
- Возвращает смешанный результат:
|
||||
```js
|
||||
{
|
||||
@@ -25,7 +25,7 @@
|
||||
- Route: `router.get('/search', requireAdmin, globalSearch)`
|
||||
- Каждая sub-query SELECT отдельно с LIMIT, общий ответ — простой json
|
||||
- Auth: admin only (teachers видят только своих учеников; для упрощения — admin)
|
||||
- [ ] Frontend: `frontend/js/admin/palette.js` — palette модуль:
|
||||
- [x] Frontend: `frontend/js/admin/palette.js` — palette модуль:
|
||||
- Не section, а глобальный widget — подключается в admin.js init
|
||||
- Слушает `keydown` на `Ctrl+K` / `Cmd+K` (preventDefault)
|
||||
- Открывает modal через `LS.modal()`:
|
||||
@@ -35,7 +35,7 @@
|
||||
- Дебаунс поиска ~150ms
|
||||
- Min длина query: 2 символа
|
||||
- При query='' → показать "Recent Actions" hardcoded list
|
||||
- [ ] Actions index (hardcoded в palette.js):
|
||||
- [x] Actions index (hardcoded в palette.js):
|
||||
```js
|
||||
const ACTIONS = [
|
||||
{ id: 'award_coins', name: 'Выдать монеты', icon: 'coins', handler: () => AdminRouter.navigate('#shop') },
|
||||
@@ -49,13 +49,13 @@
|
||||
];
|
||||
```
|
||||
- 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
|
||||
- [ ] Стили palette: глассморфизм/blur, центрировано, max-width 600px, dark/light theme-friendly. Использовать существующие токены `--surface`, `--border`, `--text-2`.
|
||||
- [ ] Подсказка в UI: footer dialog'а "↑↓ — навигация · ↵ — выбрать · esc — закрыть"
|
||||
- [x] Стили palette: глассморфизм/blur, центрировано, max-width 600px, dark/light theme-friendly. Использовать существующие токены `--surface`, `--border`, `--text-2`.
|
||||
- [x] Подсказка в UI: footer dialog'а "↑↓ — навигация · ↵ — выбрать · esc — закрыть"
|
||||
|
||||
## Files to Modify/Create
|
||||
|
||||
@@ -108,6 +108,43 @@ LS.modal сейчас принимает `{ title, body, footer, onOk, onClose,
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
<!-- Implementer: записать, какой формат ответа /admin/search,
|
||||
как palette вызывает navigate (важно для Phase 6 — deep user page будет ловить #users/N),
|
||||
какие actions zarejestrowano (Phase 6 может добавить ещё). -->
|
||||
### 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`.
|
||||
|
||||
Reference in New Issue
Block a user