# Phase 1: Hash-router

**Status:** ✅ Implemented (awaiting review)
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend

## Objective

Заложить фундамент для URL-роутинга admin-панели через `location.hash`. После этой фазы можно делать F5 на `#users`, делиться deep-links, использовать browser back/forward. Старая система табов (`switchTab`) продолжает работать без изменений — router её обёртывает, а не заменяет.

## Tasks

- [x] Создать `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`
- [x] Подключить `router.js` в `admin.html` ДО `admin.js`
- [x] В `admin.js` модифицировать `switchTab(btn)`:
  - Дополнительно вызывать `AdminRouter.navigate('#' + name, { silent: true })`
  - НЕ удалять старую логику
- [x] Добавить листенер `AdminRouter.on('change', ...)` в admin.js:
  - При route change → найти соответствующий `.admin-nav-item[data-tab="X"]` и активировать его (через имеющийся switchTab, но с `silent`-флагом чтобы избежать рекурсии)
- [x] При инициализации страницы:
  - Если `location.hash` пустой → set default `#stats`
  - Если есть hash → распарсить и переключить на соответствующий tab
- [x] Логировать unknown routes: `console.warn('AdminRouter: unknown route', route)` + fallback на `#stats`
- [x] Защита от инфинит-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

**Router API location:** `window.AdminRouter` (defined in `frontend/js/admin/router.js`, loaded **before** `admin.js` from `admin.html`).

**Public surface:**

```js
AdminRouter.parse('#users/123')
  // → { route: 'users', params: ['123'], raw: '#users/123' }
AdminRouter.current()
  // → parsed location.hash
AdminRouter.navigate('#users', { replace: false, silent: false })
  // replace → history.replaceState (no extra entry)
  // silent  → suppress synchronous 'change' emit; hashchange still fires natively
AdminRouter.on('change', ({ route, params, raw }) => { ... })
AdminRouter.off('change', fn)
```

**Events emitted:** only `'change'` for now. Payload is the parsed route plus `silent: false`. Internal `_navigating` flag suppresses re-emit when *we* set the hash (prevents the snake-eats-tail loop).

**How Phase 2 sections subscribe:**

```js
AdminRouter.on('change', ({ route, params }) => {
  if (route === 'users')   AdminSections.users.init();
  if (route === 'sessions' && params[0]) AdminSections.sessions.openDetail(params[0]);
});
```

Sections should call `AdminRouter.current()` once on load to handle the initial route (the router does NOT replay past 'change' events to late subscribers).

**switchTab contract change:**
`switchTab(btn, opts)` — `opts.fromRouter === true` prevents `switchTab` from re-pushing the hash (used by router when responding to a hashchange / deep-link). Existing call sites (`switchTab(this)`, `switchTab(qBtn)`, `switchTab(this);loadAvatarRequests()`) keep working — they call without `opts`, so the URL syncs as expected.

**Default route:** `#stats` (matches existing initially-active tab). Phase 3 will change default to `#overview` once dashboard ships.

**Unknown / locked routes:** logged via `console.warn('AdminRouter: unknown route', name)`, then `replace`-navigated to `#stats` without polluting browser history.

**Files touched:**
- `frontend/js/admin/router.js` — NEW, 97 lines
- `frontend/admin.html` — +1 line (`<script src="/js/admin/router.js"></script>` before admin.js)
- `frontend/js/admin/admin.js` — `switchTab` signature `(btn, opts)`, +6 lines for hash-sync; new ~36-line `initAdminRouter` IIFE in init block

**Backward compat verified:**
- All 21 `onclick="switchTab(this)"` callsites untouched.
- `goAddQuestion(slug)` works (calls `switchTab(qBtn)` without `opts` → URL also updates to `#questions`).
- `onclick="switchTab(this);loadAvatarRequests()"` on the avatars tab still works.
