sdlc-hub/docs/superpowers/plans/2026-10-24-web-modules.md
Andrey Epifancev 52a6c93db8 plan(web-modules): план реализации веба под модули
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:38:46 +04:00

186 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Веб под модули — план реализации
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** оболочка веба рисует состояние модулей (агент, свежесть, обновление, пакет) по их описанию, навигация сгруппирована по модулям с главной страницей «Обзор», адреса модулей с префиксом модуля, граница ядра и модулей проверяется тестами, дефекты страниц из аудита исправлены.
**Architecture:** контракт `HubModule` получает `title`, `status: ModuleStatus` (данные) и `Summary`; ядро (`web/src/shell/activity/`) держит один опрос `GET /agents` и один трекер пакетов на все модули (обобщение `RefreshProvider` модуля jira); блок состояния, «Обзор», верхняя строка узкого экрана — компоненты ядра. Бэкенд добавляет `jira_synced_at` в проект, `module` в пакет и `batches` в ответы `POST/PATCH /projects`.
**Tech Stack:** React 19 + TypeScript + Vite, TanStack Query, react-router, Vitest + Testing Library, Playwright; Python 3.13 (uv, pytest).
**Spec:** `openspec/changes/web-modules/` (proposal.md, design.md, specs/web-app, specs/hub-api) — issue jira-hub#98. Аудит и решения — design.md.
## Global Constraints
- **Предусловие:** ветка перебазирована на `main` после слияния sdlc-rename (#97): пакет `src/sdlc_hub/`, бренд «SDLC Hub», ключи `sdlc-hub-*`, `readMigrated` в `web/src/api/legacyStorage.ts`. Без этого план не начинать.
- Адреса: «Обзор» `/` (под `/app`), jira — `/jira/board`, `/jira/issues`, `/jira/issues/:key`, `/jira/feed`; wiki — `/wiki`; прежние `/board`, `/issues`, `/issues/:key`, `/feed` — перенаправление с сохранением `search`, `replace`.
- Тексты (точно): названия модулей «Jira», «Вики»; «Обзор»; агент — «агент на связи» / «агент офлайн» / «обновите агента»; свежесть — «данные на ДД.ММ ЧЧ:ММ · все проекты» / «данные на ДД.ММ ЧЧ:ММ · проект», «обновления ещё не было»; действия — «Обновить из Jira», «Обновить вики»; подсказки значков — «Обновить из Jira — все проекты», «Обновить вики — проект <метка>»; заголовок вкладки «<страница> · <метка> · SDLC Hub»; «Последние пакеты проекта»; «Команды хаба»; «Привязать корень вики»; «нужно целое число».
- События пакета — прежние тексты `refreshText` (`пакет #N в очереди/выполняется…`, `выполнен`, `не выполнен: …`, `отменён`, `…: <причина> — опрос остановлен, «<действие>» снова доступна`, `хаб не отвечает — пакет не поставлен`); `POLL_MS = 3000`, `MAX_POLL_ERRORS = 5`, `DONE_VISIBLE_MS = 10_000`; подхват и `GET /agents` — раз в `60_000` мс.
- Primary-кнопок обновления нет: значок в строке модуля, `variant="secondary"` в шапках страниц.
- Цвет не единственный признак: точка агента и просрочка — с текстом (`title`/скрытый текст).
- AURA (скилл `aura-design`): цвета только из `styles/tokens.css`, компоненты из `web/src/ui/`.
- Зелёный — весь набор: `uv run pytest -q`, `cd web && npm run lint && npm test`; перед финальным ревью ещё `cd web && npm run e2e`.
- Живой хаб (:8790) и его данные не трогать; e2e — только на тестовом хабе (`web/scripts/test-hub.sh`, :8791).
- Язык комментариев, текстов, коммитов — русский; коммиты заканчиваются `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
## Review Focus
1. Пакет, поставленный из MCP или автообновлением, во время открытой страницы: строка модуля должна его показать не позже чем через 60 с и не блокировать кнопку обновления — Task 3, `picks up batch of any author` и `foreign batch does not block refresh`.
2. Смена проекта во время идущего пакета вики: строка «Вики» нового проекта не показывает чужой пакет, опрос старого не течёт в новую строку — Task 3, `project switch drops project-scoped batch`.
3. Проект без корня вики на всех поверхностях (панель развёрнута/свёрнута, телефон, «Обзор»): нет пустой строки «Вики», нет запроса `/wiki/status` из оболочки — Task 4 `no wiki row without root`, Task 5 `overview card without root`.
4. Прежний адрес карточки из истории браузера (`/app/issues/KEY` без проекта): перенаправление и окно поверх списка, закрытие окна возвращает на `/jira/issues` с текущим проектом — Task 4, `legacy card address`.
5. Пересечение классов CSS, которое уже ломало «Вики»: тест-страж должен падать на `div.wiki`-подобном конфликте и на дубле класса между модулем и ядром — Task 2, `css classes do not collide`.
---
### Task 1: API для блока состояния
**Files:**
- Modify: `src/sdlc_hub/modules/jira/project_fields.py` (`view`: `jira_synced_at`), `src/sdlc_hub/core/queue.py` (`batch_view`: `module`; `list_batches(conn, limit=20, project=None, module=None)`), `src/sdlc_hub/core/api.py` (`h_batches_get`: `?module=`, неизвестный — 404 `not_found`; `POST /projects` и `PATCH /projects/{label}`: `batches`; `GET /` и `GET /links` → `Location: /app/`)
- Test: `tests/test_project_fields.py`, `tests/test_queue_core.py`, `tests/test_api.py`, `tests/test_web_app.py`, `tests/test_wiki_projects.py`
**Interfaces:**
- Produces: `GET /projects[].jira_synced_at: str | null` — `finished_at` последнего `sync_run` полного обновления (тот же запрос, что `last_sync` в `modules/jira/mirror.py` снимка доски — вынести в функцию `last_full_sync(conn) -> dict | None` модуля jira и использовать в обоих местах); `batch_view(...)["module"]: str | None` — `registry.module_of(<тип первой команды по seq>).name`; `POST /projects` → `{project, batch_id, batches: [{"module": str, "id": int}]}`; `PATCH` → `{project, batch_ids, batches}`.
- [ ] **Step 1: Тесты**
- `test_projects_jira_synced_at`: до полного обновления `GET /projects` → у проекта `jira_synced_at is None`; после завершённого полного `refresh` с `sync_run.finished_at = "2026-10-24T11:07:00"` → это значение у каждого проекта.
- `test_batch_module`: пакет `refresh` → `module == "jira"`; пакет `wiki_sync` → `"wiki"`; пакет без команд → `None` (в `list_batches` и `batch_view`).
- `test_batches_module_filter`: `GET /batches?module=wiki&project=AWPCA` → только пакеты wiki проекта; `GET /batches?module=nope` → 404.
- `test_create_project_batches`: `POST /projects {label, name, wiki_root: "DOC-001234"}` → `batches == [{"module": "jira", "id": <refresh>}, {"module": "wiki", "id": <wiki_sync>}]`, `batch_id == <refresh>`; `PATCH /projects/AWPCA {wiki_root}` → `batches == [{"module": "wiki", "id": batch_ids[0]}]`.
- `test_root_redirect` (в `test_web_app.py`, заменить прежний): `GET /` и `GET /links` → 302, `Location: /app/`.
- [ ] **Step 2:** `uv run pytest tests/test_project_fields.py tests/test_queue_core.py tests/test_api.py tests/test_web_app.py tests/test_wiki_projects.py -q` — FAIL.
- [ ] **Step 3:** реализация по Interfaces. В ядре (`queue.py`, `api.py`) — без названий модулей (граница `tests/test_registry.py`): модуль берётся из `registry.module_of`.
- [ ] **Step 4:** `uv run pytest -q` — весь набор PASS (в том числе `test_registry.py`, эталоны MCP: `batch_id` в MCP не меняется).
- [ ] **Step 5: Commit** — `feat(web-modules): API — свежесть Jira в проекте, модуль пакета, пакеты нового проекта по модулям`.
### Task 2: Граница ядра и модулей в вебе
**Files:**
- Create: `web/src/ui/format.ts` (+ `format.test.ts`), `web/src/modules/jira/api/types.ts`, `web/src/boundary.test.ts`
- Move: `web/src/api/closed.ts` (+ тест) → `web/src/modules/jira/api/closed.ts`
- Modify: `web/src/api/types.ts` (остаются `Batch` (+ `module: string | null`), `BatchCommand`, `BatchStatus`, `AgentView`, `CreatedBatch`, `Project`, `CreatedProject`, `UpdatedProject`, `HubSettings` без словарей jira; `fact_status` удалён везде, в том числе `web/src/test/issueFixtures.ts` и комментарий `modules/jira/issues/card.ts`), импорты типов в модулях; `modules/jira/board/model/util.ts` (`fmtStamp` переезжает в `ui/format.ts`), `modules/wiki/PageView.tsx`, `modules/wiki/SyncStatus.tsx`; `modules/jira/issues/wiki/wiki.css` и разметка, использующая `className="wiki"` в модуле jira → `jira-markup`; `IssueTable.tsx` и `IssueCard.tsx` (даты через `fmtDate`); `web/src/modules/index.ts` или `modules.test.tsx` (уникальность)
**Interfaces:**
- Produces: `fmtStamp(iso: string | null): string` — «ДД.ММ ЧЧ:ММ» (как сейчас в `board/model/util.ts`); `fmtDate(iso: string | null, now?: Date): string` — «ДД.ММ», год не текущий — «ДД.ММ.ГГГГ», `null` — «—»; `AgentView` = поля `GET /agents`: `{module: string; online: boolean; last_seen_at: string | null; agent_version: number | null; warning: string | null; update_hint: string | null; current_command_id: number | null; release: {version: number} | null}`.
- [ ] **Step 1: Тесты**
- `format.test.ts`: `fmtDate("2026-10-19", new Date("2026-10-24"))` → `"19.10"`; `fmtDate("2025-12-31", new Date("2026-10-24"))` → `"31.12.2025"`; `fmtDate(null)` → `"—"`; `fmtStamp("2026-10-24T11:07:00")` → `"24.10 11:07"`.
- `boundary.test.ts` (читает файлы через `import.meta.glob(..., { query: "?raw", eager: true })`):
- `core does not import modules`: файлы `src/{shell,ui,api,projects,auth,styles}/**/*.{ts,tsx}` (кроме тестов) не импортируют `modules/` иначе как `modules/index.ts` и `modules/types.ts`;
- `module does not import other module`: файл `modules/<a>/**` не импортирует `modules/<b>/`;
- `css classes do not collide`: множество имён классов (`/\.([a-zA-Z_][\w-]*)/g` по селекторам, без значений свойств) в `modules/<a>/**/*.css` не пересекается с `modules/<b>/**/*.css` и с `src/{shell,ui,styles,projects,auth}/**/*.css`; сообщение — имя класса и файлы;
- `core has no module terms`: в файлах ядра нет `fact_status`, `closed_statuses`, `kind_labels`, `link_types`, `BoardIssue`, `IssueRow`.
- `modules.test.tsx`: `names unique` и `nav paths unique` (T7.6) — сообщение с дублем; `NotFound` — ключ ссылки уникален при дубле (T7.5).
- [ ] **Step 2:** `cd web && npx vitest run src/ui/format.test.ts src/boundary.test.ts src/modules/modules.test.tsx` — FAIL (пересечение `.wiki`, импорт wiki → jira, термины в `api/types.ts`).
- [ ] **Step 3:** перенос типов и `closed.ts`, `ui/format.ts`, `jira-markup`, даты в таблице и карточке, удаление `fact_status`.
- [ ] **Step 4:** `cd web && npm run lint && npm test` — PASS; на странице «Вики» тестового хаба заголовок h1 — 20 px (проверить в e2e Task 8).
- [ ] **Step 5: Commit** — `refactor(web-modules): граница ядра и модулей в вебе, форматтеры дат в ядре, без fact_status`.
### Task 3: Контракт состояния модуля и трекер пакетов в ядре
**Files:**
- Create: `web/src/shell/activity/ActivityProvider.tsx`, `web/src/shell/activity/activity.ts` (контекст и хуки), `web/src/shell/activity/events.ts` (перенос `refreshStatus.ts`: `ModuleEvent`, `eventText(e, actionLabel)`, константы), `web/src/shell/activity/activity.test.tsx` (перенос и обобщение `modules/jira/refresh/refresh.test.tsx`), `web/src/modules/jira/status.ts`, `web/src/modules/wiki/status.ts`
- Modify: `web/src/modules/types.ts` (`ModuleStatus`, `HubModule.title`, `status`, `Summary`; удалить `SidebarStatus`, `TopbarNotice`, `useOnProjectCreated`), `web/src/modules/index.ts`, `web/src/modules/jira/index.tsx`, `web/src/modules/wiki/index.tsx`, `web/src/App.tsx` (провайдер ядра), `web/src/modules/wiki/api/queries.ts` (`useWikiSync` не нужен оболочке; страница вики берёт действие из трекера)
- Delete: `web/src/modules/jira/refresh/` (`RefreshProvider.tsx`, `refresh.ts`, `refreshStatus.ts`, `DataStatus.tsx`, `SidebarStatus.tsx`, их тесты) после переноса тестов; `web/src/modules/wiki/SyncStatus.tsx` (+ тест)
**Interfaces:**
- Consumes: `Batch.module`, `AgentView` (Task 2), `GET /batches?module=&project=&limit=`, `jira_synced_at` (Task 1).
- Produces:
- `ModuleStatus` — как в design.md («Контракт веб-модуля»): `agent?: string; scope: "all" | "project"; freshness(p: Project): {available: boolean; at: string | null; error: string | null}; refresh?: {label: string; start(project: string): Promise<{id: number; estimate_seconds?: number}>; isOwn(b: Batch): boolean}; invalidate(qc: QueryClient, project: string | null): void`.
- `useModuleActivity(name: string): {agent: AgentView | null; event: ModuleEvent | null; busy: boolean; running: Batch | null; start(): Promise<void>; track(id: number): void}` — `busy`: постановка в пути или отслеживаемый пакет удовлетворяет `refresh.isOwn`.
- `useAgents(): AgentView[]`; `useActivitySummary(): "error" | "busy" | "online" | "offline"` (для сводной точки).
- `ActivityProvider({children, project, pollMs?})` — `project` — текущая метка (из `ProjectContext`).
- jira: `status = {agent: "jira", scope: "all", freshness: p => ({available: true, at: p.jira_synced_at ?? null, error: null}), refresh: {label: "Обновить из Jira", start: () => api.post("/refresh", {full: true, author: "viewer"}), isOwn: b => b.commands?.[0]?.type === "refresh"}, invalidate: board-все, issues, issue, changes}`; wiki: `{agent: "wiki", scope: "project", freshness: p => ({available: !!p.wiki_root, at: p.wiki_synced_at ?? null, error: p.wiki_sync_error ?? null}), refresh: {label: "Обновить вики", start: project => api.post("/wiki/sync", {project, author: "viewer"}), isOwn: b => b.commands?.[0]?.type === "wiki_sync"}, invalidate: ["wiki", project] tree/page/search}`.
- Правила трекера: подхват — при монтировании, при смене проекта, раз в 60 с и после `start`: `GET /batches?module=<m>&limit=20` (+`&project=<метка>` для `scope: "project"`), первый незавершённый (`queued`/`running`, самый старый) — если ничего не отслеживается; при смене проекта отслеживаемый пакет модуля с `scope: "project"` сбрасывается без события. `start`: агент офлайн по `useAgents()` → `agentOffline: true` в событии; ответ 409 (`ApiError.status === 409`) — сразу подхват без события ошибки. Если отслеживается чужой пакет (не `isOwn`), `start` ставит пакет и переключает отслеживание на него. По завершении пакета — событие, `module.status.invalidate(qc, project)`, перечитывание `["projects"]`.
- [ ] **Step 1: Тесты** `activity.test.tsx` (мок `fetch` как в прежнем `refresh.test.tsx`, фейковые таймеры) — перенести все сценарии прежнего файла на модуль jira (`M-2`, `M-3`, `M-4`, `T10.2`, `T4.1`, 401, `batch-done` гаснет, переход между страницами), заменив «подхват viewer+refresh» новыми:
- `picks up batch of any author`: `GET /batches?module=jira` отдаёт пакет автора `hub` в `running` → `event.kind === "batch"`, опрос `GET /batches/{id}`;
- `foreign batch does not block refresh`: отслеживается пакет jira с `commands[0].type = "ensure_fields"` → `busy === false`; `start()` → один `POST /refresh`, `event.id` — новый пакет;
- `project switch drops project-scoped batch`: отслеживается `wiki_sync` проекта `AWPCA`, `project` → `STOCKS_DRRK` → `event === null`, опрос пакета AWPCA остановлен; jira-пакет не сброшен;
- `wiki 409 tracks running batch`: `POST /wiki/sync` → 409 → подхват идущего пакета, события ошибки нет;
- `agent offline from /agents`: `GET /agents` → wiki `online: false`; `start()` для wiki → событие с `agentOffline: true`;
- `invalidates module data only`: завершение пакета wiki инвалидирует `["wiki", "AWPCA", …]` и `["projects"]`, не `["board"]`;
- `one agents poll`: два модуля → один запрос `GET /agents` за интервал.
- [ ] **Step 2:** `cd web && npx vitest run src/shell/activity` — FAIL.
- [ ] **Step 3:** реализация. Состояние — на модуль (`Record<name, {active, event, starting, failures}>`), опросы пакетов — `useQueries` по активным; логика остановки — перенос из `RefreshProvider` без изменений.
- [ ] **Step 4:** `cd web && npm run lint && npm test` — PASS (временные заглушки оболочки, если Task 4 ещё не сделан: `Sidebar` может рендерить старый блок через `useModuleActivity` — главное, чтобы набор был зелёным).
- [ ] **Step 5: Commit** — `feat(web-modules): состояние модуля в контракте, трекер пакетов и агенты в ядре`.
### Task 4: Оболочка — группы, блок состояния, узкий экран, адреса
**Files:**
- Create: `web/src/shell/ModuleStatusBlock.tsx` (+ тест), `web/src/shell/useDocumentTitle.ts`
- Modify: `web/src/shell/Sidebar.tsx` (+ `Sidebar.test.tsx`), `web/src/shell/Layout.tsx`, `web/src/shell/NotFound.tsx`, `web/src/shell/shell.css`, `web/src/modules/jira/index.tsx` (пути `/jira/…`, перенаправления прежних путей), `web/src/modules/jira/issues/modal.ts` и `overlay.tsx` (`/jira/issues`, закрытие окна — с текущим проектом), все ссылки на `/board`, `/issues`, `/feed` внутри модулей
- Test: `web/src/shell/*.test.tsx`, `web/src/projects/projects.test.tsx`
**Interfaces:**
- Consumes: `useModuleActivity`, `useAgents`, `useActivitySummary` (Task 3); `HubModule.title`, `status`.
- Produces: `NavItem.title?: string` (название страницы для вкладки; по умолчанию `label`); `useDocumentTitle(page: string | null, project: string | null)`; `ModuleStatusBlock({project, variant: "expanded" | "collapsed"})`; оболочка: пункт «Обзор» (`to: "/"`) — первым, затем группы `MODULES` с заголовком `title`; смена проекта вне раздела → `/`.
- [ ] **Step 1: Тесты**
- `ModuleStatusBlock.test.tsx`: две строки «Jira» и «Вики» с точкой и текстом агента («агент на связи»/«агент офлайн»/«обновите агента», `warning` в `title`); «данные на 24.10 11:07 · все проекты» и «данные на 24.10 11:05 · проект»; значок с `aria-label` «Обновить из Jira — все проекты» и «Обновить вики — проект AWPCA»; `jira_synced_at: null` → «обновления ещё не было»; `wiki_sync_error: "403 Forbidden"` → текст ошибки в строке «Вики» (тон ошибки, с текстом); `no wiki row without root` — проект без `wiki_root` → одна строка, запросов `/wiki/status` и `/board` нет; `collapsed` — по значку на модуль, подробности в `title`.
- `Sidebar.test.tsx`: заголовки групп «Jira», «Вики»; «Обзор» первым; на 390 px верхняя строка — «Меню», метка проекта, сводная точка (`aria-label` с состоянием); `drawer is modal` — фокус в панели, Escape во вложенном меню закрывает только меню (M-1 #73), Escape после — панель, фокус на «Меню»; метка проекта свёрнутой панели целиком в `title` (M-4).
- `Layout`/`projects.test.tsx`: `legacy card address` — `/issues/AURORA-1?project=AWPCA` → `/jira/issues/AURORA-1?project=AWPCA`, окно поверх списка, закрытие → `/jira/issues?project=AWPCA`; `/board?project=X` → `/jira/board?project=X`; смена проекта на «Вики» у проекта без вики → остаётся «Вики» (страница с формой привязки), на неизвестном адресе → `/`; заголовок вкладки «Вики · AWPCA · SDLC Hub», «Обзор · AWPCA · SDLC Hub».
- [ ] **Step 2:** `cd web && npx vitest run src/shell src/projects` — FAIL.
- [ ] **Step 3:** реализация. Панель прокручивается сама, если не влезает (M-5 #73); удалить мёртвый `--topbar-h` (M-6), устаревшие комментарии про верхнюю панель (M-7 #73, С19 аудита); событие пакета на узком экране — строкой под верхней строкой (замена `TopbarNotice`), на развёрнутой панели — в строке модуля; `role="status"` — постоянный контейнер (T10.1 #94).
- [ ] **Step 4:** `cd web && npm run lint && npm test` — PASS.
- [ ] **Step 5: Commit** — `feat(web-modules): боковая панель по модулям, блок состояния, узкий экран, адреса модулей`.
### Task 5: Страница «Обзор»
**Files:**
- Create: `web/src/shell/OverviewPage.tsx` (+ `OverviewPage.test.tsx`), `web/src/modules/jira/Summary.tsx`, `web/src/modules/wiki/Summary.tsx`
- Modify: `web/src/shell/pages.tsx` (маршрут `/` → «Обзор» под `ProjectGate`), `web/src/modules/jira/index.tsx`, `web/src/modules/wiki/index.tsx` (`Summary`), `web/src/api/queries.ts` (`useRecentBatches(project)` → `GET /batches?project=&limit=10`)
**Interfaces:**
- Consumes: `ModuleStatusBlock` нет — карточка модуля «Обзора» рисует ядро из `useModuleActivity` + `status.freshness` + `useAgents` (версия `agent_version`), кнопка `variant="secondary"`; `Summary({project: Project})`.
- Produces: jira `Summary`: «<N> задач · <открытых> открытых · <просроченных> просрочено» (по `useBoard(project)`: свои задачи `neighbor = false`; открытые — не `closed` по `modules/jira/api/closed.ts`; просроченные — открытые с `duedate` < сегодня), ссылки «Доска связей», «Задачи», «Лента»; wiki `Summary`: «<wiki_pages> страниц», поле поиска (Enter → `/wiki?project=<метка>&q=<строка>`; страница «Вики» читает `q` в поле поиска), ссылка «Вики»; без корня — объяснение и ссылка «Привязать корень вики» на `/wiki?project=<метка>`.
- [ ] **Step 1: Тесты** `OverviewPage.test.tsx`: `/` с проектом `AWPCA` (корень есть) — две карточки «Jira», «Вики» с агентом и версией («v14», «v1»), свежестью, кнопками «Обновить из Jira», «Обновить вики» (обе `secondary`); `overview card without root` — `sfm_kedr` → карточка «Вики» с «Привязать корень вики»; «Последние пакеты проекта» — строки `#<id>`, модуль, название, автор, статус, время; поиск по вики Enter → адрес `/wiki?project=AWPCA&q=согласов`, на «Вики» строка поиска заполнена и показаны результаты.
- [ ] **Step 2:** `cd web && npx vitest run src/shell/OverviewPage.test.tsx` — FAIL.
- [ ] **Step 3:** реализация (сетка карточек: 2 колонки ≥ 1024 px, 1 колонка уже; компоненты `Panel`, `PageHeader`, `Button`).
- [ ] **Step 4:** `cd web && npm run lint && npm test` — PASS.
- [ ] **Step 5: Commit** — `feat(web-modules): страница «Обзор» со сводками модулей и последними пакетами`.
### Task 6: Окно «Новый проект» по модулям
**Files:**
- Modify: `web/src/projects/NewProjectDialog.tsx` (+ тест), `web/src/projects/projects.css`, `web/src/api/queries.ts` (`useCreateProject`, `useUpdateProject` — тип ответа с `batches`), `web/src/api/types.ts` (`CreatedProject.batches`, `UpdatedProject.batches: {module: string; id: number}[]`), `web/src/shell/Layout.tsx` (`onCreated` → `track` каждого пакета своим модулем), `web/src/modules/wiki/RootForm.tsx` (пакет из `batches` → `track` модуля wiki)
**Interfaces:**
- Consumes: `useModuleActivity(name).track` (Task 3), `HubModule.title`, `projectFields`.
- [ ] **Step 1: Тесты** `NewProjectDialog.test.tsx`: группы `fieldset` с `legend` «Jira» и «Вики»; подсказки — текст под полем (`aria-describedby`), `placeholder` пустой; подсказка метки «метка задач в Jira и имя проекта в хабе»; `51.5` в «Номер доски Jira» → «нужно целое число», запроса нет (T4.1); ответ `batches` с двумя пакетами → `track` jira и `track` wiki вызваны со своими id (M-14, T5.3 #68); `setExtra` — функциональная форма (T4.2: два быстрых ввода в разные поля не теряют первое); `FIELDS` собирается при отрисовке, не при импорте (T4.5); `useUpdateProject` и показ 422 у поля корня — тесты (T4.4).
- [ ] **Step 2:** `cd web && npx vitest run src/projects src/modules/wiki` — FAIL.
- [ ] **Step 3:** реализация.
- [ ] **Step 4:** `cd web && npm run lint && npm test` — PASS.
- [ ] **Step 5: Commit** — `feat(web-modules): «Новый проект» — поля по модулям, подсказки, пакеты каждому модулю`.
### Task 7: Страницы модулей — исправления по аудиту
**Files:**
- Modify: `web/src/modules/wiki/WikiPage.tsx`, `Tree.tsx`, `Search.tsx`, `wiki.css` (+ тесты); `web/src/modules/jira/board/components/BoardHeader.tsx`, `web/src/modules/jira/issues/IssuesPage.tsx`, `IssueTable.tsx`, `issues.css`, `IssueCard.tsx`, `web/src/modules/jira/feed/FeedPage.tsx`; `web/src/projects/ProjectGate.tsx`; `web/src/styles/` (глобальное `scrollbar-gutter: stable` для прокручиваемой области страницы)
- [ ] **Step 1: Тесты**
- «Вики»: шапка — h1 «Вики», подзаголовок «<wiki_root_title> · <N> страниц · обновлено ДД.ММ ЧЧ:ММ», кнопка «Обновить вики» `secondary` из `useModuleActivity("wiki")`; без `page` — открыт корень и выделен в дереве; узлы-предки выбранной страницы раскрыты, после очистки поиска раскрытие прежнее (T10.2, T10.6); `aria-setsize`/`aria-posinset` у узлов (T10.2); порядок заголовков h1 → h2 (T10.3); на 390 px (`matchMedia` мок) страница выше дерева; параметр `q` заполняет поиск.
- Доска, «Задачи», «Лента»: в шапке вторичная кнопка «Обновить из Jira» (`useModuleActivity("jira")`), нет primary; под шапкой доски нет пустого блока `.board-src` без содержимого (N-1), мёртвые пропсы `label`/`title` удалены (N-2 #68).
- «Задачи»: просроченный срок — с текстом «просрочено» для экранного чтения (`visually-hidden`) и `title`; срок через `fmtDate`.
- Карточка: раздел «Команды хаба» вместо «Команды моста».
- `ProjectGate`: «проект NOPE не найден» и подсказка «выберите проект в списке «Проект» боковой панели (на узком экране — в «Меню»)».
- [ ] **Step 2:** `cd web && npx vitest run src/modules src/projects` — FAIL.
- [ ] **Step 3:** реализация. Горизонтальная прокрутка «Задач» на 1440 px: таблица прокручивается внутри своей панели (`overflow-x: auto` у обёртки таблицы, ширина обёртки не больше контейнера страницы).
- [ ] **Step 4:** `cd web && npm run lint && npm test` — PASS.
- [ ] **Step 5: Commit** — `fix(web-modules): страницы модулей — шапки, «Вики» с корнем, просрочка, прокрутка «Задач»`.
### Task 8: e2e, документация, спека
**Files:**
- Modify: `web/e2e/*.spec.ts` (новые адреса, «Обзор», блок состояния, перенаправления), `web/e2e/hub.ts` (фикстура пакетов, если нужна), `ARCHITECTURE.md` (раздел «Веб»: контракт `HubModule` с `status`/`Summary`, трекер в `shell/activity/`, адреса), `CLAUDE.md` (адреса веба, «Обзор»), `README.md` (адреса веба)
- Test: `web/e2e/layout.spec.ts`, `web/e2e/screens.spec.ts`, новый `web/e2e/overview.spec.ts`
- [ ] **Step 1: Тесты e2e** (тестовый хаб): `overview.spec.ts` — `/app/` → `/app/?project=…`, две карточки, список пакетов; `layout.spec.ts` — на 1440 px у «Задач» `document.documentElement.scrollWidth === window.innerWidth`; на «Вики» `getComputedStyle(h1).fontSize === "20px"`; `/app/board` → `/app/jira/board`; на 390 px — метка проекта в верхней строке, «Меню» модально; `screens.spec.ts` — скриншоты «Обзора» и панели в обеих темах (как прочие экраны).
- [ ] **Step 2:** `cd web && npm run e2e` — новые тесты падают до правок e2e-хелперов, затем PASS.
- [ ] **Step 3:** документация.
- [ ] **Step 4:** `uv run pytest -q`, `cd web && npm run lint && npm test && npm run e2e`, `openspec validate web-modules --strict` — PASS.
- [ ] **Step 5: Commit** — `docs(web-modules): ARCHITECTURE, CLAUDE.md, README, e2e под модули`.