chore(plan): admin-redesign 6-phase plan
PLAN.md + 6 subplans + CONTEXT.md Strategy: Incremental | Mode: Automated | Execution: Orchestrator Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# Phase 1: Hash-router
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** frontend
|
||||
|
||||
## Objective
|
||||
|
||||
Заложить фундамент для URL-роутинга admin-панели через `location.hash`. После этой фазы можно делать F5 на `#users`, делиться deep-links, использовать browser back/forward. Старая система табов (`switchTab`) продолжает работать без изменений — router её обёртывает, а не заменяет.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Создать `frontend/js/admin/router.js` с `window.AdminRouter`:
|
||||
- `parse(hash)` → `{ route: 'users', params: ['123'], raw }`
|
||||
- `navigate(routeOrHash, { replace?, silent? })` — программная навигация
|
||||
- `current()` → текущий route object
|
||||
- `on(event, fn)` / `off(event, fn)` — pub/sub для 'change' event
|
||||
- Поддержка форматов: `#stats`, `#users`, `#users/123`, `#sessions/456`
|
||||
- [ ] Подключить `router.js` в `admin.html` ДО `admin.js`
|
||||
- [ ] В `admin.js` модифицировать `switchTab(btn)`:
|
||||
- Дополнительно вызывать `AdminRouter.navigate('#' + name, { silent: true })`
|
||||
- НЕ удалять старую логику
|
||||
- [ ] Добавить листенер `AdminRouter.on('change', ...)` в admin.js:
|
||||
- При route change → найти соответствующий `.admin-nav-item[data-tab="X"]` и активировать его (через имеющийся switchTab, но с `silent`-флагом чтобы избежать рекурсии)
|
||||
- [ ] При инициализации страницы:
|
||||
- Если `location.hash` пустой → set default `#stats`
|
||||
- Если есть hash → распарсить и переключить на соответствующий tab
|
||||
- [ ] Логировать unknown routes: `console.warn('AdminRouter: unknown route', route)` + fallback на `#stats`
|
||||
- [ ] Защита от инфинит-loop'а: флаг `_routerNavigating` при programmatic-навигации, чтобы handler не реагировал на свой же hash change
|
||||
|
||||
## Files to Modify/Create
|
||||
|
||||
- `frontend/js/admin/router.js` — новый, ~80-120L
|
||||
- `frontend/admin.html` — добавить `<script src="/js/admin/router.js"></script>` в `<head>` или перед admin.js
|
||||
- `frontend/js/admin/admin.js` — модифицировать `switchTab` + добавить init-логику (~15-25L изменений)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- F5 на `http://localhost:3000/admin#users` восстанавливает users-tab
|
||||
- Browser back/forward переключают между табами (без полного reload)
|
||||
- Клик по admin-nav-item обновляет URL (`#users` появляется в адресной строке)
|
||||
- Клик по cross-tab handler типа `goAddQuestion('bio')` — старая логика работает, URL обновляется
|
||||
- Unknown hash (например `#nonexistent`) → console.warn + fallback на `#stats`, нет crash
|
||||
- `#users/123` парсится корректно (params=['123']), но пока никто его не использует — Phase 6 подключит
|
||||
|
||||
## Notes
|
||||
|
||||
### Почему hash-router, а не history.pushState
|
||||
|
||||
Backend Express раздаёт admin.html по `/admin`. С `pushState` пришлось бы либо настраивать catch-all route на server-стороне (`/admin/*`), либо делать SPA-style роутинг. Hash-router работает out-of-the-box и не требует backend-изменений. Это критично для incremental-стратегии — мы не трогаем server в Phase 1.
|
||||
|
||||
### Защита от рекурсии
|
||||
|
||||
Сценарий: пользователь кликает на tab → switchTab вызывает navigate → navigate меняет hash → срабатывает hashchange → router emits 'change' → handler вызывает switchTab → snake eats tail.
|
||||
|
||||
Решение:
|
||||
```js
|
||||
let _navigating = false;
|
||||
function navigate(hash) {
|
||||
_navigating = true;
|
||||
location.hash = hash;
|
||||
_navigating = false;
|
||||
}
|
||||
window.addEventListener('hashchange', () => {
|
||||
if (_navigating) return;
|
||||
// emit 'change'
|
||||
});
|
||||
```
|
||||
|
||||
Или передавать `{ silent: true }` через объект-параметр и проверять его в handler'е switchTab.
|
||||
|
||||
### Существующий пример hashchange
|
||||
|
||||
В `frontend/js/textbook-tracker.js:438` уже есть `addEventListener('hashchange', handleHashNav)` — это safe-pattern, можно подсмотреть структуру.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- [ ] router.js не использует Grep / эмоджи / отсутствующие LS-помощники
|
||||
- [ ] Старый switchTab НЕ удалён, только обёрнут
|
||||
- [ ] Нет регрессий: все 13 табов переключаются, lazy-load работает
|
||||
- [ ] F5 / back / forward проверены вручную в браузере (или симуляция через subagent)
|
||||
- [ ] Default route `#stats` срабатывает при пустом hash
|
||||
- [ ] Unknown route не крашит панель
|
||||
- [ ] Код следует конвенциям проекта (no emoji, inline SVG для иконок, LS.* для API)
|
||||
- [ ] Build passes: `cd backend && npm start` → http://localhost:3000/admin загружается
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
<!-- Заполнит implementer после фазы.
|
||||
Должно содержать: где живёт router API, какие события эмитятся, как Phase 2 sections подписываются на route changes. -->
|
||||
Reference in New Issue
Block a user