Hover-only action buttons (right-aligned, opacity transition, hidden on mobile). - users.js: 4 actions (ban/unban, award coins, sessions, delete) — replaces `>` glyph cell, falls back to glyph for non-admin / self - sessions.js: 2 actions (view, delete) - DELETE /api/admin/sessions/:id (NEW): transactional (assignment_sessions=NULL, user_answers, session_questions, test_sessions), audit-logged, admin-only - event.stopPropagation defence-in-depth (each button + parent .row-actions) - LS.confirm for destructive ops; LS.modal for award-coins amount/reason - CSS injected once via #row-actions-style id-dedup (same content in both sections) Existing user-panel overlay + session toggle-drawer flows untouched (Phase 6 removes overlay). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.7 KiB
Phase 5: Per-row quick actions
Status: ✅ Done Parent plan: 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):- Добавлена
<td class="row-actions-cell">с inline-flex блоком.row-actions(заменяет старый›индикатор) - Visible: только на
:hoverстроки (CSS opacity transition) - Кнопки (inline SVG, Lucide-style):
- Ban / Unban —
quickToggleBan(uid, isBanned, btn)→LS.confirm→LS.adminBanUser - Award coins —
quickAwardCoins(uid, name)→LS.modal(sm) с inputs amount+reason →LS.adminShopAwardCoins - Sessions —
quickOpenUserSessions(uid)→AdminRouter.navigate('#sessions')(fallback наswitchTab) - Delete —
quickDeleteUser(uid, name, btn)→LS.confirm(destructive) →LS.adminDeleteUser
- Ban / Unban —
- SVG-иконки (inline, Lucide outline-style), НЕТ эмоджи
event.stopPropagation()на каждой кнопке + на родительском.row-actions(чтобы не открывать user-panel overlay)- Hidden для self (
u.id !== user.id) и для non-admin — fallback на старый›
- Добавлена
- Sessions table (
frontend/js/admin/sections/sessions.js):- View (eye icon) —
toggleDrawer(id)(тот же flow что и row-click) - Delete (trash, danger) —
quickDeleteSession(id, btn)→LS.confirm→LS.adminDeleteSession→load()(refresh)
- View (eye icon) —
- Backend
DELETE /api/admin/sessions/:id— endpoint отсутствовал, добавлен:- Route:
backend/src/routes/admin.js(внутриrequireRole('admin')блока) - Controller:
deleteSession(req, res, next)вadminController.js— транзакция:UPDATE assignment_sessions SET session_id = NULL WHERE session_id = ?(explicit null, hoarded slot stays)DELETE FROM user_answers WHERE session_id = ?(FK hasON DELETE CASCADE, но делаем явно)DELETE FROM session_questions WHERE session_id = ?(то же)DELETE FROM test_sessions WHERE id = ?
- Audit:
audit(req, 'session.delete', 'session:${sid}', 'user:N mode:X') - Validates
Number.isInteger(sid) && sid > 0; 404 if not found
- API helper:
LS.adminDeleteSession(id)→DELETE /admin/sessions/:id
- Route:
- CSS (
#row-actions-style):- Inject ONCE из обеих секций (de-dup по element id) — оба
ensureRowActionsStyles()проверяютgetElementById('row-actions-style')перед добавлением - Стили:
.row-actions,.row-action-btn(default + .danger),.row-actions-cell,@media (max-width: 768px) { display: none } - Также handle
tr.selected .row-actionsи.sess-tl-item.open .row-actions→ opacity 1 (для активных строк)
- Inject ONCE из обеих секций (de-dup по element id) — оба
title="…"на каждой кнопке (tooltip)LS.confirm(message, { title, confirmText })использован везде (signature:lsConfirm(message, { title, confirmText, danger=true })—danger:truedefault, gradient pink→violet)
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 —
opacity: 0 → 1без display swap, занимают слот старого› - Имя пользователя в onclick экранируется через
esc()+replace(/'/g, "\\'")для безопасности SQL/HTML-injection в строковых литералах - No emoji — только inline SVG (Lucide-style outline-stroke, viewBox 24x24)
event.stopPropagation()на каждой кнопке + на родительском.row-actionsdiv (defence in depth)- Confirm через
LS.confirmдля destructive (delete user, delete session, ban/unban) titleатрибут есть на каждой кнопке- Mobile (≤768px):
.row-actions { display: none }— row-click overlay по-прежнему работает как fallback node --checkall modified files OK- Tests: 32/35 pass (3 pre-existing auth-test failures, unrelated)
Handoff to Next Phase
Phase 6 (deep entity pages) рекомендации:
-
quickOpenUserSessions(uid)сейчас просто навигирует на#sessionsбез фильтра. Phase 6 должна:- Расширить router до
#sessions?user=N(или новый формат#sessions/user/N) - В
sessions.jsload()читать query param и передаватьuser_idвLS.adminGetSessions({ user_id })(backend уже поддерживаетuser_idquery param — см.getAllSessionscontroller) - Обновить хелпер:
AdminRouter.navigate('#sessions?user=' + uid)(когда router научится parse'ить query)
- Расширить router до
-
User-panel overlay vs hover actions: Phase 6 удалит старую
.user-paneloverlay. Когда это произойдёт, row-click больше не будет открывать панель. Hover-actions останутся как primary UX. Рекомендация: при удалении overlay row-click сделатьonclick="AdminRouter.navigate('#users/' + uid)"(deep page). -
Mobile UX gap: на ≤768px actions сейчас полностью скрыты. Когда Phase 6 добавит deep page, mobile-row-click станет переходом на deep page → primary actions доступны там. До тех пор mobile = read-only browse.
-
Backend
DELETE /admin/sessions/:idуже там, готов для Phase 6 deep session page (где будет кнопка "Удалить эту сессию" в header). -
Award coins modal pattern (используем
LS.modalс body=DOM Node + actions сonClick({close, setError})) — может быть полезен Phase 6 для inline-edit flow на deep user page. -
Linter note:
npm run lint:routesпоказывает FAIL (65 unprotected vs baseline 56) — pre-existing проблема, my new admin-protectedDELETE /sessions/:idдобавил +1 false-positive (роут защищён черезrouter.use(requireRole('admin'))блок, который linter не видит). Не требует действий — это known limitation скрипта.