diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index be276e6..4530ed8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -89,25 +89,32 @@ общие части (`web/src/ui/` — компоненты дизайн-системы AURA, `web/src/styles/tokens.css` — токены и единственное место с литералами цвета, `web/src/api/` — клиент HTTP, `web/src/auth/`, `web/src/projects/` — проект в адресе `?project=<метка>`, выпадающий список, окно «Новый проект») и модули `web/src/modules/<имя>/`. Контракт веб-модуля — `HubModule` в -`web/src/modules/types.ts` (`pages`, `nav`, `Provider`, `SidebarStatus`, `TopbarNotice`, `useOverlay`, `useOnProjectCreated`); +`web/src/modules/types.ts` (`name`, `title`, `pages`, `nav`, `Provider`, `status`, `Summary`, `useOverlay`, `projectFields`); список модулей — константа `MODULES` в `web/src/modules/index.ts` (хуки вызываются в постоянном порядке). +Состояние модуля — данные, а не компонент: `status` (`ModuleStatus`) описывает агента (`agent`), охват (`scope`: `all`/`project`), свежесть +(`freshness(project)`), действие обновления (`refresh`: подпись, `start`, `isOwn`) и перечитывание после пакета (`invalidate`). Рисует и ведёт +его ядро: трекер пакетов и агентов — `web/src/shell/activity/` (`ActivityProvider`: подхват идущего пакета и `GET /agents` раз в 60 с, +опрос пакета каждые 3 с, `useModuleActivity(module)`, тексты `events.ts`/`agentText.ts`), блок состояния панели — `ModuleStatusBlock`, а модуль +даёт только `Summary` — карточку на «Обзоре» (`OverviewPage`: карточки модулей и «Последние пакеты проекта»). Адреса: «Обзор» — `/app/`, +страницы модуля — `/app/<модуль>/…` (`/app/jira/board`, `/app/jira/issues[/{key}]`, `/app/jira/feed`, `/app/wiki`); прежние `/app/board`, +`/app/issues`, `/app/issues/{key}`, `/app/feed` перенаправляются с сохранением `search` и `replace`. + Модуль `wiki` (`web/src/modules/wiki/index.tsx`): страница «Вики» (`/app/wiki`): дерево (`Tree`) с поиском (`Search`), страница (`WikiPage`, `PageView` — `react-markdown` + `remark-gfm`, без `rehype-raw`; ссылки `page:` ведут на страницу вики, `attachment:` скачивают файл через `download.ts`, `urlTransform.ts` пропускает только эти схемы и http(s)), форма корня без привязки (`RootForm`), -плашка синхронизации в боковой панели (`SyncStatus`); поле «Корень вики» в окне «Новый проект» — через `projectFields`. +сводка для «Обзора» (`Summary`) и `status` для ядра; поле «Корень вики» в окне «Новый проект» — через `projectFields`. -Модуль `jira` (`web/src/modules/jira/index.tsx`): страницы «Доска связей» (`/app/board`), «Задачи» (`/app/issues`), «Лента» -(`/app/feed`), карточка `/app/issues/{key}` окном поверх страницы (`issues/overlay.tsx`, `IssueModal.tsx`; оболочка рисует +Модуль `jira` (`web/src/modules/jira/index.tsx`): страницы «Доска связей» (`/app/jira/board`), «Задачи» (`/app/jira/issues`), «Лента» +(`/app/jira/feed`), карточка `/app/jira/issues/{key}` окном поверх страницы (`issues/overlay.tsx`, `IssueModal.tsx`; оболочка рисует страницу по `location.state.background`). Внутри: `board/` (чистые `model`, `analysis`, `layout`, `storage.ts` — ключи `localStorage` прежней страницы, у проекта AWPCA прежние, у остальных с суффиксом `@<метка>`; отрисовка `components/`; golden-эталоны `__fixtures__/`), `issues/` (список, карточка, `wiki/` — разбор вики-разметки, `attachments/` — список, плашки и скачивание: `POST /attachments/{id}/fetch`, опрос `GET /attachments/{id}`, затем `api.blob` на `/content`), -`feed/` (период, виды, группировка событий, `EventList`, общий с блоком «История» карточки), `refresh/` (кнопка и плашка -обновления данных из Jira в боковой панели, отслеживание пакета: `RefreshProvider`, `useJiraRefresh`), `api/queries.ts`. +`feed/` (период, виды, группировка событий, `EventList`, общий с блоком «История» карточки), `RefreshButton.tsx` (кнопка «Обновить из Jira» в шапках страниц), `Summary`/`status` для ядра, `api/queries.ts`. Собирается `make web` в `src/sdlc_hub/web/app/`; `core/api` отдаёт сборку без токена (`/app/assets/*` — файлы, прочие -`/app…` — `index.html`, 503 без сборки), `/` и `/links` перенаправляет на `/app/board`. Данные — только через HTTP API с +`/app…` — `index.html`, 503 без сборки), `/` и `/links` перенаправляет на `/app/` («Обзор»). Данные — только через HTTP API с клиентским токеном; `mirror.board` отдаёт зеркало без преобразований, перевод в модель доски — в `board/model`. Цепочки зависимостей (Python): diff --git a/CLAUDE.md b/CLAUDE.md index 06c66f9..8c66522 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ SDLC-хаб с модулями jira и wiki. Мост Jira для АРМ КА: - `src/sdlc_hub/modules/wiki/` — модуль wiki: `schema`, `settings`, `paths`, `projects` (поле `wiki_root`), `commands` (`wiki_sync`, `wiki_fetch` и хуки очереди), `convert` (HTML → Markdown), `stage`, `sync`, `search` (FTS5), `attachments`, `tick`, `api`, `mcp`. Входная точка — `MODULE` в `modules/wiki/__init__.py`. - Граница: ядро не импортирует модули (кроме ленивого импорта в `registry.py` и шагах миграций `db.py`), типы команд и термины модулей (jira, wiki, eva) живут в модуле, новые имена в ядре — без названий модулей; проверяют `test_core_imports`, `test_core_has_no_jira_terms`, `test_core_has_no_module_terms` в `tests/test_registry.py`. Меток-исключений нет. - Цепочка ядра: `db` ← `settings`, `projects` ← `queue` ← `api` ← `cli`; `files` ← `queue`, `api`; реле `relay` — поток в `serve`; `backup` вызывается из `api` и из `db`. Модуль jira: `hooks → commands/mirror/history/refresh/attachments/fetch → core`. -- Веб: оболочка и общее — `web/src/{shell,ui,api,projects,auth,styles}`, модули — `web/src/modules/<имя>/` (контракт `HubModule` в `modules/types.ts`, список — `modules/index.ts`); модуль jira — страницы доски, задач и ленты; модуль wiki (`web/src/modules/wiki/`) — страница «Вики» (`/app/wiki?project=<метка>`). +- Веб: оболочка и общее — `web/src/{shell,ui,api,projects,auth,styles}`, модули — `web/src/modules/<имя>/` (контракт `HubModule` в `modules/types.ts`, список — `modules/index.ts`); состояние модулей (агент, свежесть, обновление) ведёт ядро в `web/src/shell/activity/`, модуль даёт `status` и `Summary` для «Обзора»; модуль jira — страницы доски, задач и ленты; модуль wiki (`web/src/modules/wiki/`) — страница «Вики» (`/app/wiki?project=<метка>`). - Подробная карта модулей и границ — `ARCHITECTURE.md`. ## Команды @@ -26,7 +26,7 @@ SDLC-хаб с модулями jira и wiki. Мост Jira для АРМ КА: - `uv sync` — установка окружения. - `uv run sdlc-hub init --config PATH [--force]` — настройки проекта из `config.example.json`, создание токенов. - `uv run sdlc-hub serve [--host] [--port]` — локальный запуск хаба. -- Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board?project=<метка>`, задачи — `/app/issues?project=<метка>`, лента изменений — `/app/feed?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску. +- Веб-приложение — `http://127.0.0.1:8790/app/` («Обзор» — `/app/?project=<метка>`, доска — `/app/jira/board?project=<метка>`, задачи — `/app/jira/issues?project=<метка>`, лента изменений — `/app/jira/feed?project=<метка>`, вики — `/app/wiki?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на «Обзор», прежние `/app/board`, `/app/issues`, `/app/feed` — на страницы модуля Jira. - `make web` — сборка приложения (после `git pull` на Mac mini, до перезапуска хаба); `make check` — pytest + lint и тесты фронта; `cd web && npm run dev` — разработка с прокси на тестовый хаб `bash web/scripts/test-hub.sh` (:8791); `cd web && npm run e2e` — Playwright; `cd web && npm run golden` — перегенерировать эталоны доски из `links.html@3931a88` (только при смене фикстур). - `uv run sdlc-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run sdlc-hub mcp <модуль> [--project <метка>]` — MCP модуля по stdio (модуль обязателен: `jira` — сервер `sdlc-jira`, `wiki` — сервер `sdlc-wiki`; с привязкой сессии к проекту); `uv run sdlc-hub tokens` — показать токены (клиентский и по агенту на модуль). - `uv run sdlc-hub agent publish [--module jira|wiki] PATH` — опубликовать версию агента модуля для самообновления (только вверх; по умолчанию `jira`; публикация лежит в `release/<модуль>/`). diff --git a/README.md b/README.md index 52a817a..841a706 100644 --- a/README.md +++ b/README.md @@ -212,14 +212,14 @@ uv run sdlc-hub agent publish --module wiki ../eva-agent/src/agent.py | `GET /agent/known-keys` | агент | ключи зеркала | | `GET /agent/release` | агент | опубликованная версия агента `{version, sha256, content}`; 404 `no_release` | | `GET /agent/files/{sha256}` | агент | файл вложения `{file, size, content}` (`content` — base64); 404 `no_file` | -| `GET /projects`, `POST /projects` | клиент | проекты `{projects: […]}` (метка, название, `board_id`, `board_type`, число задач) / завести `{label, name, board_id?, author?}` → 201 `{project, batch_id}` и пакет «Новый проект <метка>» (`refresh` по общей JQL); 409 `project_exists`, 422 `bad_project`, 422 `bad_batch` (постановка `refresh` отказана) | +| `GET /projects`, `POST /projects`, `PATCH /projects/{метка}` | клиент | проекты `{projects: […]}`: метка, название, `created_at` и поля модулей — jira: `board_id`, `board_type`, `issues_count`, `jira_synced_at` (время последнего полного обновления из Jira; `null` — ещё не было), wiki: `wiki_root`, `wiki_synced_at`, `wiki_sync_error`. `POST` заводит `{label, name, поля модулей?, author?}` → 201 `{project, batch_id, batches}`: пакеты, поставленные модулями (`batches: [{module, id}]`; у jira — «Новый проект <метка>», `refresh` по общей JQL; `batch_id` — пакет первого модуля); 409 `project_exists`, 422 `bad_project`, 422 `bad_batch` (постановка отказана). `PATCH` меняет изменяемые поля модулей (иные — 422 `bad_project`) → 200 `{project, batch_ids, batches}` | | `GET /issues` | клиент | фильтры `project` (метка; неизвестная — 404), `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`, `hide_closed` (`1` — без задач в статусах из настройки `closed_statuses`, по умолчанию «Готово», «Снято»), `limit`, `offset` | | `GET /issues/{key}` | клиент | задача целиком, с комментариями и журналом изменений `changes` (новые сверху) | | `GET /changes` | клиент | лента изменений: `since`, `until` (ISO со смещением или `Z`, иначе 422; по умолчанию — последние 24 ч), `project` (только свои задачи), `key`, `kinds`, `limit` (до 2000) → `{since, until, truncated, changes}` | | `GET /board` | клиент | `?project=<метка>` (неизвестная — 404): снимок для доски — свои задачи проекта, соседи (связи в обе стороны) и эпики своих задач, с `fact_status`/`work_kind`; связи — те, у которых хотя бы один конец — своя задача проекта (без `project` — все задачи и связи), `last_sync` — последний успешный полный прогон | -| `GET /`, `GET /links` | — | перенаправление на `/app/board` | +| `GET /`, `GET /links` | — | перенаправление на `/app/` («Обзор») | | `PUT /issues/{key}/note` | клиент | заметки (частично) | -| `POST /batches`, `GET /batches`, `GET /batches/{id}` | клиент | пакеты команд; `project` в теле `POST /batches` — пакет проекта (проверки метки, ограничение задач проектом), `?project=` у `GET /batches` — только пакеты проекта | +| `POST /batches`, `GET /batches`, `GET /batches/{id}` | клиент | пакеты команд; `project` в теле `POST /batches` — пакет проекта (проверки метки, ограничение задач проектом), `?project=` у `GET /batches` — только пакеты проекта, `?module=<имя>` — только пакеты модуля (по типу первой команды; неизвестный модуль — 404), `limit`; в каждом пакете поле `module` (`null` — не определён) | | `POST /batches/{id}/cancel` | клиент | отмена пакета | | `POST /commands/{id}/retry`, `POST /commands/{id}/cancel` | клиент | повтор (новый пакет; `project` в теле — отказ, если команда не из этого проекта) / отмена (`force` для взятой) | | `POST /refresh` | клиент | `{keys}`, `{jql}` или `{full: true}` (общая JQL по меткам всех проектов; проектов нет — 422 «нет проектов») | @@ -234,8 +234,8 @@ curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8790/issues?sprint= ## Веб-приложение -Адреса: `http://127.0.0.1:8790/app/` — доска `/app/board?project=<метка>`, задачи `/app/issues?project=<метка>`, лента изменений `/app/feed?project=<метка>` (период и виды событий, история — и в карточке задачи), карточка `/app/issues/{key}` — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); `/` и -`/links` перенаправляют на доску. Вход по клиентскому токену (`uv run sdlc-hub tokens`), токен хранится в браузере; +Адреса: `http://127.0.0.1:8790/app/` — «Обзор» `/app/?project=<метка>` (карточки модулей Jira и «Вики» с состоянием агента, свежестью данных и обновлением, последние пакеты проекта), доска `/app/jira/board?project=<метка>`, задачи `/app/jira/issues?project=<метка>`, лента изменений `/app/jira/feed?project=<метка>` (период и виды событий, история — и в карточке задачи), вики `/app/wiki?project=<метка>`, карточка `/app/jira/issues/{key}` — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); `/` и +`/links` перенаправляют на «Обзор»; прежние адреса `/app/board`, `/app/issues`, `/app/feed` ведут на страницы модуля Jira. Вход по клиентскому токену (`uv run sdlc-hub tokens`), токен хранится в браузере; `localhost` и `127.0.0.1` — разные сайты для браузера, токен и раскладка у каждого свои. Выкатка на Mac mini: ``` diff --git a/docs/superpowers/plans/2026-10-24-web-modules.md b/docs/superpowers/plans/2026-10-24-web-modules.md new file mode 100644 index 0000000..6a6be1d --- /dev/null +++ b/docs/superpowers/plans/2026-10-24-web-modules.md @@ -0,0 +1,185 @@ +# Веб под модули — план реализации + +> **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 `. + +## 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": }, {"module": "wiki", "id": }]`, `batch_id == `; `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//**` не импортирует `modules//`; + - `css classes do not collide`: множество имён классов (`/\.([a-zA-Z_][\w-]*)/g` по селекторам, без значений свойств) в `modules//**/*.css` не пересекается с `modules//**/*.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; 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=&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`), опросы пакетов — `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`: « задач · <открытых> открытых · <просроченных> просрочено» (по `useBoard(project)`: свои задачи `neighbor = false`; открытые — не `closed` по `modules/jira/api/closed.ts`; просроченные — открытые с `duedate` < сегодня), ссылки «Доска связей», «Задачи», «Лента»; wiki `Summary`: « страниц», поле поиска (Enter → `/wiki?project=<метка>&q=<строка>`; страница «Вики» читает `q` в поле поиска), ссылка «Вики»; без корня — объяснение и ссылка «Привязать корень вики» на `/wiki?project=<метка>`. + +- [ ] **Step 1: Тесты** `OverviewPage.test.tsx`: `/` с проектом `AWPCA` (корень есть) — две карточки «Jira», «Вики» с агентом и версией («v14», «v1»), свежестью, кнопками «Обновить из Jira», «Обновить вики» (обе `secondary`); `overview card without root` — `sfm_kedr` → карточка «Вики» с «Привязать корень вики»; «Последние пакеты проекта» — строки `#`, модуль, название, автор, статус, время; поиск по вики 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 «Вики», подзаголовок « · страниц · обновлено ДД.ММ ЧЧ:ММ», кнопка «Обновить вики» `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 под модули`. diff --git a/openspec/changes/web-modules/.openspec.yaml b/openspec/changes/web-modules/.openspec.yaml new file mode 100644 index 0000000..1f5d280 --- /dev/null +++ b/openspec/changes/web-modules/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-24 diff --git a/openspec/changes/web-modules/design.md b/openspec/changes/web-modules/design.md new file mode 100644 index 0000000..09312d3 --- /dev/null +++ b/openspec/changes/web-modules/design.md @@ -0,0 +1,169 @@ +## Context + +Основа — `main` после слияния sdlc-rename (#97): пакет `sdlc_hub`, бренд «SDLC Hub», ключи `sdlc-hub-*`. Ветка +`feat/98-web-modules` перебазируется на него до исполнения плана. + +Аудит веба 24.10.2026 (сабагент, код + живой хаб, скриншоты в scratchpad сессии) — главное: +- контракт `HubModule` (`web/src/modules/types.ts`): `name`, `pages`, `nav`, `Provider`, `SidebarStatus` + (компонент), `TopbarNotice`, `useOverlay`, `projectFields`, `useOnProjectCreated(batchId)`; +- `RefreshProvider` (модуль jira, 174 строки) — единственный трекер пакетов: подхват пакетов `viewer` с первой + командой `refresh` (по `GET /batches/{id}` на каждого кандидата), один опрос, остановка, перечитывание; агент — `GET + /agent` (только jira); время — `last_sync` из снимка `/board` (грузится на каждой странице); +- wiki: `SyncStatus` — текст из `GET /projects` + `GET /wiki/status`, кнопка только в шапке страницы; +- ядро знает jira: `/` → `/board` в модуле jira, типы jira и `fact_status` в `api/types.ts`, `api/closed.ts`, бренд; + модуль wiki импортирует `fmtStamp` из модуля jira; +- `modules/jira/issues/wiki/wiki.css` (`div.wiki h1…`) перебивает заголовки страницы «Вики» (корень `div.wiki`); +- `POST /projects` → один `batch_id`, оболочка раздаёт его всем модулям; +- API: `GET /agents` уже есть (все поля агента), `GET /projects` уже несёт свежесть wiki, у пакета нет модуля. + +## Goals / Non-Goals + +**Goals:** оболочка рисует состояние модулей по их описанию; пакеты и агенты отслеживает ядро; навигация и главная +страница — модульные; граница ядра и модулей в вебе проверяется тестами; найденные аудитом дефекты страниц исправлены. + +**Non-Goals и Anti-Goals:** см. proposal.md. + +## Decisions + +1. **Навигация — группы по модулям + «Обзор»** (вариант «А + Б» аудита). Группы оставляют все разделы на виду (их + четыре), «Обзор» даёт модульную главную вместо `/board` и место для сводки. Переключатель модулей (вариант «В») + отклонён: лишний щелчок между доской и вики ради масштаба, которого нет. +2. **Адреса с префиксом модуля** (`/app/jira/…`, `/app/wiki`) и перенаправления со старых. Делается сейчас, пока + ломается всё сразу (переименование); пути модулей не пересекаются по построению. Перенаправления объявляет модуль + jira (это его прежние адреса). `GET /` хаба — на `/app/`. +3. **Состояние модуля — данные (`ModuleStatus`), рисует ядро.** Компонент `SidebarStatus` убирается: из-за него блок и + был разнобойным. Свежесть берётся из уже загруженного `GET /projects` чистой функцией от проекта — без запросов + модуля: jira — `jira_synced_at` (новое поле, общее для проектов), wiki — `wiki_synced_at`/`wiki_sync_error`. +4. **Один трекер пакетов в ядре** (`shell/activity/`): обобщение `RefreshProvider` — состояние на модуль + (`event`, `active`, `busy`), опрос `GET /batches/{id}`, правила остановки прежние. Подхват — по полю `module` из + `GET /batches?module=` (новое), любого автора: автообновления хаба и пакеты из MCP теперь видны (С1/В1 аудита), а + подхват не делает запрос на каждого кандидата. Прежнее правило «только viewer + refresh» отменено (сценарий «Пакет с + другими командами» меняется: пакет показывается, но не блокирует кнопку). +5. **Кнопка недоступна только на время пакета своего действия обновления** (jira — первая команда `refresh`, wiki — + `wiki_sync`; тип виден в `GET /batches/{id}`, который трекер и так опрашивает). Иначе долгое скачивание вложения + блокировало бы «Обновить из Jira». +6. **Обновление: значок в строке модуля + вторичная кнопка в шапке страниц модуля**, primary для обновления нет. Сейчас + на «Вики» две зелёные кнопки (в панели и в шапке) — против правила AURA «одна primary на экран». +7. **Агенты — один опрос `GET /agents`** раз в 60 с; точка агента: на связи / офлайн / «обновите агента» (`warning` + или `update_hint`), всегда с текстом. +8. **Узкий экран**: в верхней строке «Меню», метка проекта и сводная точка; отдельная кнопка «Обновить из Jira» и + `TopbarNotice` из контракта убираются (событие пакета показывает ядро строкой под верхней строкой). Выезжающая + панель — модальная (#73 M-1…M-3). +9. **Пакеты нового проекта по модулям**: `POST`/`PATCH /projects` отдают `batches: [{module, id}]`; `batch_id` и + `batch_ids` остаются для MCP. Оболочка передаёт каждому модулю его пакет (снимает M-14/T5.3 #68 и потерю `wiki_sync`). +10. **Граница в вебе — тестами**: импорты (ядро → только `modules/index.ts`; модуль → не другой модуль), уникальность + имён модулей и `nav.to` (T7.5/T7.6 #90), пересечение имён классов CSS между модулями и с ядром. Префиксы классов + не вводятся: тест на пересечение ловит именно случившийся дефект и не требует переименовывать сотни классов. + `div.wiki` разметки Jira переименовывается в `.jira-markup`. +11. **Типы jira — в модуль jira** (`modules/jira/api/types.ts`), `closed.ts` — туда же; `fact_status` удаляется из веба + (веб-часть #60). В `api/types.ts` ядра остаются `Batch`, `AgentView` (полный вид из `GET /agents`), `Project`, + `CreatedProject`, `UpdatedProject`, `HubSettings` без словарей jira. +12. **Форматтеры дат — ядро** (`ui/format.ts`): `fmtStamp` («ДД.ММ ЧЧ:ММ»), `fmtDate` («ДД.ММ», не текущий год — + «ДД.ММ.ГГГГ»). Списки и карточка перестают выводить ISO. +13. **«Обзор» берёт сводку у модуля** (`Summary({project})`); счётчики jira — из снимка доски проекта (запрос только на + «Обзоре»); вики — `wiki_pages` из проекта. +14. **Порядок архива**: sdlc-rename архивируется раньше web-modules: дельты `web-app`/`hub-api` этого change написаны + поверх текстов после переименования. + +## Контракт веб-модуля (после change) + +```ts +type ModuleStatus = { + agent?: string; // имя модуля агента в GET /agents + scope: "all" | "project"; // охват свежести и обновления + freshness(p: Project): { available: boolean; at: string | null; error: string | null }; + refresh?: { + label: string; // «Обновить из Jira», «Обновить вики» + start(project: string): Promise<{ id: number; estimate_seconds?: number }>; // 409 → ApiError, ядро подхватит идущий + isOwn(b: Batch): boolean; // пакет этого действия (по commands[0].type) + }; + invalidate(qc: QueryClient, project: string | null): void; // что перечитать после пакета модуля +}; +type HubModule = { + name: string; title: string; // «jira» / «Jira» + pages: RouteObject[]; nav: NavItem[]; // nav.to — с префиксом модуля + status?: ModuleStatus; + Summary?: ComponentType<{ project: Project }>; // карточка «Обзора» + Provider?, useOverlay?, projectFields?; // как было; SidebarStatus, TopbarNotice, useOnProjectCreated — удалены +}; +``` + +## Risks / Trade-offs + +- **Подхват пакетов любого автора** покажет и пакеты, поставленные из чата (`claude`), — это желаемо, но строка «Jira» + будет чаще «занята». Кнопка при этом не блокируется (решение 5). +- **Счётчики на «Обзоре»** грузят снимок доски — тяжёлый запрос; только на этой странице и с тем же ключом кэша, что у + доски. +- **Смена адресов** ломает закладки — их держат перенаправления; внешних ссылок на веб нет (MCP веб-адресов не отдаёт). + +## Decisions made without discussion + +Владелец 24.10.2026 ответил «делай» на вопросы brainstorming — решения 1–14 выше приняты агентом по аудиту. + +| # | Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---|---| +| 1 | 24.10.2026 | Навигация — группы + «Обзор» | 4 раздела, 2 модуля; нужна модульная главная | переделка панели | +| 2 | 24.10.2026 | Адреса `/app/<модуль>/…` с перенаправлениями | заодно с переименованием, пути не пересекаются | вернуть пути, перенаправления наоборот | +| 3 | 24.10.2026 | Состояние модуля — данные, рисует ядро | разнобой блоков возник из компонента | — | +| 4 | 24.10.2026 | Трекер в ядре, подхват любого автора по `module` | видны автообновления и пакеты MCP | вернуть фильтр по автору | +| 5 | 24.10.2026 | Блокирует только пакет своего обновления | долгие пакеты не мешают обновлению | — | +| 6 | 24.10.2026 | Обновление — значок в панели + secondary в шапке | одна primary на экран | перекраска кнопок | +| 7 | 24.10.2026 | Один опрос `/agents` | агент wiki не виден сейчас | — | +| 8 | 24.10.2026 | Узкий экран: метка проекта + сводная точка | проект не виден, две кнопки обновления | вернуть кнопку | +| 9 | 24.10.2026 | `batches` по модулям, старые поля остаются | пакет wiki терялся; MCP не ломать | — | +| 10 | 24.10.2026 | Тест на пересечение классов CSS вместо префиксов | ловит дефект без массового переименования | ввести префиксы позже | +| 11 | 24.10.2026 | Типы jira — в модуль, `fact_status` из веба удалён | граница ядра; #60 | вернуть поле в тип | +| 12 | 24.10.2026 | Форматтеры дат в ядре | три формата дат, импорт wiki → jira | — | +| 13 | 24.10.2026 | Сводка «Обзора» от модуля | ядро не знает данных модулей | — | +| 14 | 24.10.2026 | Архив после sdlc-rename | дельты поверх новых имён | правка дельт при архиве | +| 15 | 24.10.2026 | `last_full_sync(conn, base_jql=None)` в `modules/jira/mirror.py`: базовая JQL необязательна (по умолчанию `projects.base_jql`), `board()` передаёт свою; импорт в `project_fields.view` отложенный | интерфейс плана `(conn)` сохранён, `board()` не меняет сигнатуру; прямой импорт даёт цикл mirror → history → projects → project_fields | вернуть явный параметр | +| 16 | 24.10.2026 | Фильтр `module` в `list_batches` применяется до `limit` перебором пакетов (по типу первой команды), без SQL-колонки | у пакета нет колонки модуля, схему не менять; объём пакетов невелик | добавить колонку `batch.module` | +| 17 | 24.10.2026 | Тесты Task 1 лежат в `test_wiki_projects.py`/`test_project_fields.py` (нужны оба модуля), а не в `test_queue_core.py`/`test_api.py`; правлены старые ожидания в `test_db.py` (`jira_`-поля) | в `test_queue_core` модули подменены заглушкой | перенести тесты | +| 18 | 24.10.2026 | R-1: трейлер коммитов ветки — `Co-Authored-By: Claude Sonnet 5.5`, а не `Opus 5.5` из плана | системная подсказка сессии об атрибуции приоритетнее текста плана | переписать трейлеры | +| 19 | 24.10.2026 | R-2: предусловие плана («после слияния #97 в main») считается выполненным базированием на `feat/97-sdlc-rename` | указание владельца; PR #99 ещё не слит | ребейз на main после слияния #99 | +| 20 | 24.10.2026 | R-3: параметр `q` страницы «Вики» реализует Task 5, Task 7 только добавляет тест | в плане требование задвоено между Task 5 и Task 7 | мелкая правка | +| 21 | 24.10.2026 | Task 2: `ModuleProviders.tsx` перенесён из `modules/` в `shell/` (импортирует только `modules/index.ts`) | тест-страж границы допускает из ядра лишь `modules/index.ts` и `modules/types.ts`; `App.tsx` импортировал `modules/ModuleProviders.tsx` | вернуть файл и ослабить страж | +| 22 | 24.10.2026 | Task 2: `fmtDate` доски (`board/model/util.ts`, принимает не-ISO даты Jira, год не показывает) остаётся в модуле; в ядре `ui/format.ts` — свой `fmtDate`, таблица и карточка задач берут его (даты другого года — `ДД.ММ.ГГГГ`); `fmtStamp(null)` → «—» | задача требует форматтеры в ядре; доска сравнивает строки дат Jira в разных локалях | свести две функции в одну | +| 23 | 24.10.2026 | Task 2: словари jira-настроек — тип `JiraSettings extends HubSettings` в `modules/jira/api/types.ts`; `HubSettings` ядра — только `project_key` и индексная подпись | страж запрещает термины jira в ядре | вернуть словари в `HubSettings` | +| 24 | 24.10.2026 | Task 3: `ActivityProvider` стоит в `Layout` (внутри `ProjectContext`), а не в `App.tsx`; «проект создан» — `trackBatch(batchId)` ядра (модуль берётся из `Batch.module`, запасной путь — `refresh.isOwn`) вместо `useOnProjectCreated` модуля | `project` провайдера берётся из `ProjectContext`, он создаётся в `Layout`; модуль не называется в ядре | перенести провайдер выше и передавать проект иначе | +| 25 | 24.10.2026 | Task 3: подхват пакетов — только при монтировании, смене проекта, раз в 60 с и после 409; прежний подхват «после загрузки снимка доски» (`skipPickup`, T10.1/T4.1) убран | ядро не знает ключей данных модуля; правила подхвата заданы спекой; счётчик `skipPickup` больше не нужен | вернуть подписку на кэш по ключу, объявляемому модулем | +| 26 | 24.10.2026 | Task 3: состояние на модуль — хук-трекер на модуль (`useTracker`, вызывается по константному списку `TRACKED`) и `useQuery` пакета в каждом, а не общий `useQueries` | перенос логики остановки опроса без изменений; `useQueries` потребовал бы переписать эффект завершения | заменить на `useQueries` | +| 27 | 24.10.2026 | Task 3: `start` берёт агента свежим `fetchQuery(["agents"], staleTime 0)`, а не из кэша; ошибка этого запроса — событие `hub-down`/`error`, как у прежнего `GET /agent` | сохраняет поведение прежних сценариев (hub-down, 401 на предпроверке) | брать из кэша `useAgents()` | +| 28 | 24.10.2026 | Task 3: wiki `refresh.start` приводит ответ `{batch_id}` к `{id}`; `track()` перехватывает отслеживание, если текущий пакет не «свой» (раньше — только если ничего не отслеживалось) | контракт ядра единый `{id}`; пакет нового проекта важнее чужого | отдельная форма ответа / прежняя проверка | +| 29 | 24.10.2026 | ~~Task 3: сводка `useActivitySummary`: error > offline > busy > online~~ — заменено решением 56 | ошибка и офлайн агента важнее, чем «идёт пакет» | поменять порядок | +| 30 | 24.10.2026 | Task 3: временный `shell/activity/StatusBlock.tsx` (строки модулей и строка событий узкого экрана) вместо `SidebarStatus`/`TopbarNotice` модулей; Task 4 заменяет его; страница вики берёт действие из трекера, уведомление об ошибке постановки (`syncError`) убрано — ошибка видна событием модуля | оболочка должна работать и набор быть зелёным до Task 4 | вернуть Notice на странице вики | +| 31 | 24.10.2026 | Task 3, fix T3.I1: подхват после 409 (`pickup(true)`) заменяет отслеживаемый чужой пакет первым (самым старым) другим незавершённым пакетом модуля; если другого нет — чужой остаётся | детали 409 (`batch_id`) клиент не разбирает, а список без команд не различает типы | брать `batch_id` из `ApiError.detail` | +| 32 | 24.10.2026 | Task 4: «Обзор» (`/`) пока рисует `shell/OverviewPage.tsx` — заглушка с заголовком под `ProjectGate`; Task 5 заменяет её страницей со сводками | пункт «Обзор» и адрес `/` нужны панели и перенаправлениям уже сейчас | оставить `/` → `/jira/board` до Task 5 | +| 33 | 24.10.2026 | Task 4: пункты панели ядра — `shell/nav.tsx` (`OVERVIEW`, `GROUPS`, `NAV`); NotFound, Layout и Sidebar берут их оттуда; группа — `role="group"` с видимым названием модуля (в свёрнутой панели название скрыто) | один источник списка разделов для панели, «не найдено» и заголовка вкладки | свести в `MODULES.flatMap` по месту | +| 34 | 24.10.2026 | Task 4: список под окном карточки по прямому/прежнему адресу берёт `project` из адреса карточки (`listBackground(search)`), закрытие — `/jira/issues?project=…` | брифом: «закрытие окна — с текущим проектом»; константа `LIST_BACKGROUND` без проекта теряла выбор | вернуть константу, проект подставит Layout | +| 35 | 24.10.2026 | Task 4: модальность выезжающей панели — атрибут `inert` на соседях `aside` внутри `.shell` (страница и верхняя строка), фокус на «Меню» возвращается эффектом после закрытия; `Dropdown` гасит Escape (`preventDefault`) только когда сам открыт, а панель закрывается по Escape лишь при `!defaultPrevented` | ловушка Tab на списке фокусируемых хрупка; `inert` закрывает и чтение с экрана | ловушка фокуса вручную | +| 36 | 24.10.2026 | Task 4: `--topbar-h` удалён из `shell.css` и из `board.css` (`.drawer` на ширине страницы < 852 px статичен, а строка приложения бывает только на < 640 px) | M-6 #73: переменная мертва | вернуть переменную | +| 37 | 24.10.2026 | Task 4: прокручивается середина панели (`.sidebar__scroll`: разделы и блок состояния), низ с темой и меню «Ещё» остаётся вне прокрутки; поправка R-1 решения #69 (меню «Ещё» свёрнутой панели выходит за 64 px, при `overflow` на всей панели обрезалось бы) | M-5 #73: на низком окне панель не должна обрезаться | прокрутка всей панели и обрезанное меню | +| 38 | 24.10.2026 | Task 4: сводная точка узкого экрана — `role="img"` с `aria-label` «Состояние модулей: ошибка / идёт обновление / агент на связи / агент офлайн»; «идёт обновление» — нейтральный цвет (токена «info» нет); событие пакета узкого экрана — постоянный `role="status"` под строкой (`data-testid="topbar-events"`) | цвет не единственный признак; T10.1 #94 | вернуть `aria-hidden`-строку | +| 39 | 24.10.2026 | Task 5: `GET /board` на `/` («Обзор» грузит снимок доски для сводки jira, решение 13) — тест `no wiki row without root` блока состояния проверяет только отсутствие `GET /wiki/status` | `open()` теста открывает `/`, а он теперь тянет доску; суть теста — строка вики без корня | тест строже, чем нужно; ослабление скрывает запрос статуса вики без корня | +| 40 | 24.10.2026 | Task 5: общий `agentText(a)` (`shell/activity/agentText.ts`) — текст, подсказка и признак «на связи» агента; им пользуются блок состояния и карточки «Обзора» | текст «агент на связи/офлайн/обновите агента» не должен разойтись между двумя местами | рефакторинг блока состояния; тесты блока его держат | +| 41 | 24.10.2026 | Task 5: `useRecentBatches(project)` опрашивает `GET /batches?project=&limit=10` раз в 15 с (`refetchInterval`) | ход пакетов меняется без участия страницы; инвалидация из активности ядра усложнила бы контракт | лишние запросы раз в 15 с на «Обзоре» | +| 42 | 24.10.2026 | Task 5: поле поиска вики на «Обзоре» не ведёт на пустой запрос (Enter при пустой строке ничего не делает); `q` в «Вики» — только начальное значение поля, выбор страницы убирает `q` из адреса | пустой поиск = страница «Вики» без `q`, для неё есть ссылка «Вики»; `hrefOf` страницы по контракту Task 4 не несёт поиск | пользователь, вставший на `?q=` и открывший страницу, теряет в адресе строку поиска (в поле она остаётся) | +| 43 | 24.10.2026 | Task 6: номер доски проверяется в окне сам (`/^\d+$/`, ≥ 1, текст «нужно целое число»), поле — `type="text"` с `inputMode="numeric"`, форма `noValidate` | `type="number"` даёт браузерную подсказку вместо нашего текста и пропускает `51.5`/`1e3` по-разному в браузерах; T4.1 | поле без стрелок счётчика; невалидное значение ловится только по «Создать» | +| 44 | 24.10.2026 | Task 6: в ядре `useTrackModuleBatch()` (activity.ts) — отслеживание пакета по имени модуля; окно создания вынесено в `shell/NewProject.tsx`; `useTrackBatch` остаётся для пакетов без известного модуля | пакеты нового проекта привязываются каждому своему модулю по `batches[].module` (M-14, T5.3 #68); Layout не обязан знать модули | пакет неизвестного модуля молча не отслеживается | +| 45 | 24.10.2026 | Task 7: узкий экран «Вики» (страница выше дерева) — через `matchMedia("(max-width: 759.98px)")` и порядок в разметке (`WikiPage`), а не CSS `order`; дерево при поиске скрыто (`hidden`), а не размонтировано | порядок Tab совпадает с видимым; раскрытые вручную узлы переживают поиск (T10.2, T10.6); порог окна — приближение порога контейнера страницы (заменено решением 51: мерится ширина контейнера) | CSS `order` и размонтирование дерева | +| 46 | 24.10.2026 | Task 7: заголовки текста страницы вики сдвинуты ниже заголовка страницы: `#`/`##` → h3, `###` → h4 … не глубже h6 | порядок h1 → h2 → h3 без скачков и без второго h1 (T10.3) | оставить h1–h4 как есть | +| 47 | 24.10.2026 | Task 7: `.issue-table-wrap{overflow-x:auto}` на любой ширине (раньше — только `@container page < 1052px`) | на 1440 px девять столбцов шире панели; цена указана неверно: обёртка-скроллер выключает липкость шапки совсем — см. решение 50 | вернуть условие и сжать столбцы | +| 48 | 24.10.2026 | Task 7: проверка T5.2 «ядро не грузит /board и /wiki/status» идёт на странице вне модулей (`/app/nowhere`), а не на `/wiki?project=…` | страница «Вики» сама запрашивает `/wiki/status` (это модуль, не ядро) | проверять на странице вики с исключением статуса | +| 49 | 24.10.2026 | Task 7: `scrollbar-gutter: stable` — на `html` (прокручивается окно, отдельной области страницы нет); «Обновить из Jira» в шапках — общий `modules/jira/RefreshButton.tsx` | страницы разной высоты не двигаются по ширине; одна реализация кнопки на три страницы | прокручиваемый контейнер страницы; кнопка в каждой странице | +| 50 | 24.10.2026 | Task 7, fix T7.I1: обёртка таблицы «Задач» при высоте окна ≥ 700 px получает `max-height: calc(100vh - 220px)` и `overflow-y:auto`, шапка липнет к обёртке; на низких окнах ограничения нет — шапка уходит со страницей | при `overflow-x:auto` обёртка — скроллер, и `position:sticky` шапки внутри неё без собственной вертикальной прокрутки не работает (решение 47 ошибочно считало, что шапка липнет к панели); на длинных списках шапка должна оставаться на виду; цена — вложенная вертикальная прокрутка (на телефонах высотой < 700 px её нет) | снять sticky/z-index и принять уходящую шапку | +| 51 | 24.10.2026 | Task 7, fix T7.I2: порядок «страница/дерево» в `WikiPage` определяется `ResizeObserver` на `.wiki__body` с порогом `≤ 759.98` px — тем же, что `@container page` в `wiki.css`; `matchMedia` убран | окно и контейнер страницы (рядом боковая панель) расходятся, на окне ≈ 760–1050 px CSS уже в одну колонку, а DOM ещё нет; до первого замера (и без `ResizeObserver`) — широкая раскладка; ключи панелей сохраняют состояние дерева | вернуть `matchMedia` с запасом по ширине панели | +| 52 | 24.10.2026 | Task 7, fix T7.I2: `useWikiDownload` возвращает объект через `useMemo` | перерисовка `WikiPage` (замер ширины) пересоздавала `{start,status}` → набор компонентов Markdown → ссылки-вложения перемонтировались (тест M-15 обнаружил); так же ломали бы их ввод в поиск и опрос статуса | не трогать хук и убрать состояние замера | +| 53 | 24.10.2026 | Task 8: проверка «без горизонтальной прокрутки» на 1440 px — `document.documentElement.scrollWidth <= window.innerWidth`, а не `===` | при классической полосе прокрутки Chromium `scrollWidth` равен ширине без полосы (1425 против 1440), равенство невозможно; смысл проверки — отсутствие переполнения — сохранён | ложное «зелёное» только если переполнение скрыто тем же зазором; на 390 px (оверлейные полосы) проверка прежняя и строгая | +| 54 | 24.10.2026 | Task 8, дефект: у `.issue-table-wrap` добавлен `position: relative` | скрытый текст «, просрочено» (`.visually-hidden`, `position:absolute`) не имел позиционированного предка внутри обёртки и выходил за её прокрутку: на 390 px `scrollWidth` страницы 778 при окне 390 | без правки на узком экране у «Задач» появлялась горизонтальная прокрутка всей страницы | +| 55 | 24.10.2026 | Task 8: «Последние пакеты проекта» проверяются пакетом «Обновить вики» (пакет проекта), а не «Обновить из Jira»; для событий пакета Jira в e2e — хелпер `jiraEvent` (строка модуля в панели / полоса на узком экране) | пакет «Обновить из Jira» не привязан к проекту (охват «все проекты») и в список проекта не попадает; на странице несколько `role=status`, прежний `getByRole("status")` неоднозначен | только тесты; список пакетов проекта и так фильтруется хабом по `project` | +| 56 | 24.10.2026 | Final fix wave, I-2: сводная точка — error > busy > offline > online (заменяет решение 29) | спека «Оболочка и адреса» — источник истины: ошибка, затем «пакет в работе», затем агенты на связи или нет | перепутанный порядок красит точку «офлайн» при идущем пакете; спека и код расходятся | +| 57 | 24.10.2026 | Final fix wave, M-6/T2.3: офлайн-агент модуля сводку не красит, если `freshness(project).available === false` (вики без корня); проект для проверки провайдер берёт из `useProjects()` (тот же ключ запроса, без нового запроса); пока проекты не загружены — агент считается значимым | ядру нечего показывать и обновлять у модуля без данных проекта; `freshness(p: Project)` не менялась | кратковременно (до загрузки проектов) возможна «офлайн» у проекта без корня вики | +| 58 | 24.10.2026 | Final fix wave, T2.3: `jira_synced_at` убрано из ядрового `Project` и перенесено в `JiraProject` (`modules/jira/projectFields.ts`); `jira/status.ts` читает поле через type guard `hasJiraSync(p)` без `as`; `boundary.test.ts` ловит имена полей модулей в ядре (`MODULE_TERMS` расширен) | ядро не должно знать поля модулей; контракт `freshness(p: Project)` сохранён — минимальное решение без обобщения `ModuleStatus` | проект без поля (старый ответ хаба) показывается как «обновления ещё не было» | +| 59 | 24.10.2026 | Final fix wave, M-11: в `agentText.ts` предупреждение/`update_hint` важнее офлайна (офлайн-агент с предупреждением показывается как «обновите агента») | предупреждение требует действия человека и объясняет, почему агент не работает, а «офлайн» может пройти сам | человек увидит «обновите агента» у агента, который ещё и офлайн; после обновления текст сменится на «офлайн» | +| 60 | 24.10.2026 | Final fix wave, M-9: принято как известное ограничение — `jira_synced_at` сбрасывается после создания проекта: `last_full_sync` сравнивает JQL прогона с текущей базовой JQL, а при новом проекте она меняется; до нового полного прогона у всех проектов «обновления ещё не было». Кодом не чинится, follow-up | правка — в хабе (Python), вне объёма веб-change | у всех проектов после создания нового до следующего «Обновить из Jira» нет даты свежести | +| 61 | 24.10.2026 | Final fix wave, #68 M-2: меню доски названо «Ещё — доска»; меню панели остаётся «Ещё» | однозначные доступные имена двух меню на одной странице; тесты больше не зависят от гонки | пользовательские сценарии, искавшие «Ещё» на доске, ищут новое имя | +| 62 | 24.10.2026 | Final fix wave, #73 M-2: выезжающая панель обёрнута в `div.sidebar-modal[role=dialog][aria-modal][aria-label=Меню]` (`display: contents`), `