diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 988e97a..87600b0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,40 +1,42 @@ # Архитектура -Хаб моста Jira: SQLite-зеркало задач AURORA/AWPCA, служебные заметки, очередь команд, HTTP API и MCP. Агент (`jira-agent`, виртуалка) опрашивает хаб, пишет в Jira и возвращает состояние; хаб Jira не вызывает. +Хаб моста Jira: SQLite-зеркало задач проектов-меток Jira-проекта AURORA, служебные заметки, очередь команд, HTTP API и MCP. Агент (`jira-agent`, виртуалка) опрашивает хаб, пишет в Jira и возвращает состояние; хаб Jira не вызывает. ## Модули | Модуль | Ответственность | Зависит от | |---|---|---| | `paths` | каталог данных (`JIRA_HUB_HOME`), пути базы и `backups/`, права 0700 (`secure_dir`) | stdlib | -| `db` | схема SQLite, миграции по `schema_version` (копия базы перед миграцией), `connect` | `backup`, `paths` | +| `db` | схема SQLite, миграции по `schema_version` (копия базы перед миграцией), `connect` | `backup`, `paths`, `projects` (ленивый импорт в миграции v3→v4) | | `settings` | настройки (`setting`), токены, темп, перенос из `config.example.json` | `db` | -| `mirror` | зеркало задач, связей и комментариев, `apply_state` (с проверкой `check_comments`), заметки, фильтры, снимок доски `board` | `db` | +| `projects` | проекты-метки: таблица `project` (метка, название, `board_id`, `board_type`), проверка метки, базовая JQL из меток проектов (`base_jql`), доски (`boards`), миграция схемы v3→v4 (`migrate_v3`) | `db`, `settings` | +| `mirror` | зеркало задач, связей и комментариев, разрез по проекту (свои, соседи по связям, эпики своих), `apply_state` (с проверкой `check_comments`), заметки, фильтры, снимок доски `board` | `db` | | `commands` | контракт типов команд, валидация, временные ключи, оценка, подтверждение по состоянию | `mirror` | -| `queue` | пакеты, проверки постановки (`_check_setup`: `bug` без настройки, файл вложения), выдача (`claim`, `size` у `ensure_attachment` — `deliver_params`), отчёты, отмена, повтор, автообновление | `commands`, `mirror`, `settings`, `files`, `db` | +| `queue` | пакеты, проверки постановки (`_check_setup`: `bug` без настройки, файл вложения; `_check_project`: проверки пакета проекта — метка, чужие метки, задачи вне проекта; `_assign_boards`: `board_id` проекта у `ensure_sprint`), выдача (`claim`, `size` у `ensure_attachment` и `boards` у `refresh` — `deliver_params`), отчёты, отмена, повтор, автообновление | `commands`, `mirror`, `settings`, `files`, `projects`, `db` | | `backup` | копии файла базы, ротация | `paths` | | `files` | хранилище файлов вложений `<данные>/files/` (каталог — параметр, `Hub.files_dir`): `put` (атомарно, одна копия на содержимое, mtime при повторе, предел 10 МБ, пустой — отказ), `path`/`exists`/`size`/`read` (только sha из 64 hex нижнего регистра), `sweep` (старше 30 дней, кроме файлов `ensure_attachment` в `queued`/`claimed`) | `paths` | | `api` | HTTP-сервер, маршруты, авторизация по токенам, фоновый `tick` (автообновление и уборка `files/` раз в сутки); маршруты файлов `POST /files`, `GET /agent/files/{sha256}` (каталог `Hub.files_dir`, 0700 при старте); раздача веб-приложения `/app…` (каталог `web/app` пакета, без токена) и перенаправление `/`, `/links`; снимок `GET /board` | `queue`, `mirror`, `settings`, `backup`, `files`, `relay`, `release` | | `relay` | реле общей папки (настройка `relay_dir`): поток в `serve`, файлы `req/` → свой HTTP (адрес `serve`, для `0.0.0.0` — `127.0.0.1`) → `resp/`; агент с `hub_url = folder:<путь>` ходит через него; HTTP-обработчики о нём не знают | `settings` | | `release` | опубликованная версия агента: `release/agent.py` + `manifest.json` в каталоге данных, `publish` (только вверх; запись с fsync, прежний файл на время записи — `agent.py.prev`, при сбое возвращается), `current` (проверка манифеста, предупреждение один раз на ошибку), `load` (с проверкой sha256 и UTF-8), подсказка `hint` для `agent_status` | `paths` | | `client` | HTTP-клиент хаба | `db`, `paths`, `settings` (только для `from_home`) | -| `mcp_server` | инструменты MCP по stdio; `batch_create` читает файлы `ensure_attachment.path` и загружает их `POST /files` до постановки пакета | `client`, `files` (только `MAX_SIZE`) | +| `mcp_server` | инструменты MCP по stdio; привязка сессии к проекту (`jira-hub mcp --project <метка>`), инструменты `projects` и `project_create` (фрагмент `.mcp.json`); `batch_create` читает файлы `ensure_attachment.path` и загружает их `POST /files` до постановки пакета | `client`, `files` (только `MAX_SIZE`) | | `importer` | разовый импорт CSV-пакета в заметки, сверка с зеркалом | `mirror`, `settings` | | `cli` | `init`, `serve`, `import`, `mcp`, `tokens`, `agent publish` | `api`, `mcp_server`, `importer`, `release`, `settings`, `db`, `paths` | `web/` — веб-приложение хаба (React + TypeScript + Vite): вход по клиентскому токену, страницы «Доска» (`/app/board`) -и «Задачи» (`/app/issues`); карточка `/app/issues/{key}` — окно поверх страницы, с которой её открыли: `shell/Layout.tsx` рисует +и «Задачи» (`/app/issues`); текущий проект — `?project=<метка>` в адресе (`web/src/projects/`: контекст проекта, выпадающий список «Проект», окно «Новый проект», заглушка «проект не найден»; страницы пересоздаются по `key={project}`); карточка `/app/issues/{key}` — окно поверх страницы, с которой её открыли: `shell/Layout.tsx` рисует страницу по адресу из `location.state.background` (`useRoutes` с явным адресом) и `issues/IssueModal.tsx` поверх неё. Собирается `make web` в `src/jira_hub/web/app/`; `api` отдаёт сборку без токена (`/app/assets/*` — файлы, прочие `/app…` — `index.html`, 503 без сборки), `/` и `/links` перенаправляет на `/app/board`. Данные — только через HTTP API с клиентским токеном; `mirror.board` отдаёт зеркало без преобразований, перевод в модель доски — в `web/src/board/model`. Логика доски — чистые модули `web/src/board/{model,analysis,layout}` -и `storage.ts` (ключи `localStorage` прежней страницы), отрисовка — `web/src/board/components`. +и `storage.ts` (ключи `localStorage` прежней страницы; у проекта AWPCA — прежние, у остальных с суффиксом `@<метка>`; набор ключей — состояние модуля, выставляется при монтировании страницы), отрисовка — `web/src/board/components`. -Цепочки: `api → queue → commands/mirror/settings → db`; `db → backup → paths`, `db → paths`; `files → paths`; `api → relay → settings`; `mcp_server → client → HTTP API`; `importer → mirror/settings`. +Цепочки: `api → queue → commands/mirror/settings/projects → db`; `db → projects` (только миграция, ленивый импорт); `projects → settings`; `db → backup → paths`, `db → paths`; `files → paths`; `api → relay → settings`; `mcp_server → client → HTTP API`; `importer → mirror/settings`. ## Потоки данных - Запись в Jira: клиент (`mcp_server`/интерфейс) → `POST /batches` → `queue` → агент `claim` → Jira → `POST /agent/commands/{id}/result`. - Вложение: файл → `POST /files` → `files/` → `ensure_attachment {key, file, filename}` в пакете (`queue._check_setup` проверяет, что файл есть) → `claim` (хаб дописывает `size`, в базе `params` без него) → агент `GET /agent/files/{sha256}` → Jira; уборка `files/` — из `tick` раз в сутки. +- Создание проекта: `POST /projects` (веб или `project_create`) → `projects.create` и пакет «Новый проект <метка>» (`refresh` по общей JQL, `batch.project = NULL`) в одной транзакции → `claim` (хаб дописывает в `refresh` доски проектов, `boards`) → агент определяет тип доски → отчёт `refresh` пишет `board_type` → `ensure_sprint` проекта получает `board_id`. - Чтение Jira: команда `refresh` → агент → `POST /agent/state` → `mirror.apply_state`. - Агент за VPN: агент → файлы в общей папке (`relay_dir`) → `relay` → HTTP хаба → ответ файлом. - Самообновление агента: `jira-hub agent publish` → `release/` → `claim.agent_release` → агент скачивает `GET /agent/release`; состояние обновления агент шлёт в `claim`, хаб держит в памяти (вместе с признаком `agent_reported` — был ли `claim` с запуска хаба) и отдаёт в `GET /agent`. diff --git a/CLAUDE.md b/CLAUDE.md index e3be21b..6d8c44a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # jira-hub ## Что это -Хаб моста Jira для АРМ КА: единственный источник истины по задачам проекта AURORA (метка AWPCA) на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; активные change — `openspec/changes/` (кроме `archive/`). +Хаб моста Jira для АРМ КА: единственный источник истины по задачам проектов-меток Jira-проекта AURORA на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; активные change — `openspec/changes/` (кроме `archive/`). ## Стек и ограничения - Веб-приложение — `web/` (React 19 + TypeScript + Vite, TanStack Query, Vitest, Playwright; Node 24, npm); сборка в `src/jira_hub/web/app/` не в git. @@ -26,7 +26,7 @@ - `uv run jira-hub serve [--host] [--port]` — локальный запуск хаба. - Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board`, задачи — `/app/issues`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску. - `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 jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp` — MCP по stdio; `uv run jira-hub tokens` — показать токены. +- `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp [--project <метка>]` — MCP по stdio (с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены. - `uv run jira-hub agent publish PATH` — опубликовать версию агента для самообновления (только вверх). ## Тесты diff --git a/README.md b/README.md index 5d139a9..692cce4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # jira-hub -Хаб моста Jira для АРМ КА: зеркало задач проекта AURORA (метка AWPCA) в SQLite, служебные заметки, очередь команд, +Хаб моста Jira для АРМ КА: зеркало задач проектов-меток Jira-проекта AURORA (AWPCA и др.) в SQLite, служебные заметки, очередь команд, HTTP API в локальной сети и MCP-сервер для Claude. Хаб работает на Mac mini (`192.168.0.106`); в Jira ходит только агент на виртуалке (репозиторий `jira-agent`), пароль Jira хабу неизвестен. Спека: `openspec/changes/bridge-core/` (proposal, design, specs). @@ -59,9 +59,25 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj После перезапуска хаба или обновления кода MCP — `/mcp` → reconnect. +Сессия MCP работает в разрезе одного проекта-метки. Привязка — `--project <метка>` в `.mcp.json` репозитория проекта +(метка сравнивается точно, с учётом регистра): + +```json +{"mcpServers": {"jira-hub": {"command": "uv", "args": ["run", "--project", "/Users/andrey.epifancev/projects/jira-bridge/jira-hub", "jira-hub", "mcp", "--project", "AWPCA"]}}} +``` + +Без привязки (user scope, без `--project`) работают только служебные инструменты (`projects`, `project_create`, +`agent_status`, `sync_log`, `settings_get`, `settings_set`, `batches`, `batch_status`, `batch_wait`, `batch_cancel`); +`issues`, `issue`, `notes_set`, `batch_create`, `command_retry` и `refresh` +отвечают «сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)». Метка, которой нет в +хабе, — «проект <метка> не заведён в хабе: project_create или исправьте --project в .mcp.json». В привязанной сессии +`issues`, `batches` и `notes_set` видят только свой проект (`notes_set` — свои задачи и временные ключи `new:`), +`issue` открывает любую задачу по ключу, пакеты и команды ставятся с проектом: хаб сам дописывает метку проекта в +`ensure_issue` и отвергает чужую метку. + | Инструмент | Параметры | Результат | |---|---|---| -| `issues` | фильтры как у `GET /issues`, `limit` | таблица задач | +| `issues` | фильтры как у `GET /issues`, `limit` | таблица задач проекта сессии | | `issue` | `key` | поля, связи, заметки, последние команды | | `notes_set` | `key`, поля заметок | обновлённые заметки | | `batch_create` | `title`, `commands` (у `ensure_attachment` вместо `file` можно `path` — абсолютный путь к локальному файлу до 10 МБ) | `id`, оценка длительности, число команд | @@ -70,18 +86,25 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj | `batches` | `limit` | последние пакеты | | `command_retry`, `batch_cancel` | `id` | новый пакет / результат отмены | | `refresh` | `keys` или `full`, `wait` (по умолчанию `false`: только ставит пакет; `true` — ждёт до 30 мин) | id пакета или итог пакета | +| `projects` | нет | проекты хаба: метка, название, доска, тип доски, число задач; проект сессии отмечен | +| `project_create` | `label`, `name`, `board_id` (необязательно) | проект заведён, пакет обновления `#N`, готовый фрагмент `.mcp.json` для репозитория проекта | | `agent_status` | нет | онлайн/офлайн, версия, текущая команда, 429 за сутки | | `sync_log` | `limit` | последние загрузки состояния | | `settings_get`, `settings_set` | ключ, значение | значение / новая `settings_version` | -Автор команд из MCP: `claude`; из API без автора: `api`. +Автор команд из MCP: `claude`; из API без автора: `api`; из веб-приложения: `viewer`. + +В примере у `ensure_issue` нет `labels`: в привязанной сессии метку проекта дописывает хаб. Новый проект — `project_create`: +в ответе «проект <метка> «<название>» заведён, пакет обновления #N», затем указание положить в корень репозитория проекта +`.mcp.json` (слить с имеющимся) и переподключить MCP (`/mcp` → reconnect, одобрить сервер проекта) и сам фрагмент `.mcp.json` +с командой `uv run --project <корень хаба> jira-hub mcp --project <метка>`. «Ошибка» с файлом одним пакетом (нужна настройка `issue_types.bug`, см. «Контракт команд»): MCP читает файл по `path`, загружает его `POST /files` и ставит `ensure_attachment` с `file` и `filename` (по умолчанию — имя файла). Нет файла, нет прав, не файл, больше 10 МБ — ответ с причиной и путём, пакет не ставится. ```json -[{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "bug", "summary": "ОСВ 60 не распознана", "labels": ["AWPCA"]}}, +[{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "bug", "summary": "ОСВ 60 не распознана"}}, {"type": "ensure_sprint", "params": {"key": "new:1", "sprint": "Спринт 2"}}, {"type": "ensure_attachment", "params": {"key": "new:1", "path": "/Users/…/ОСВ сч. 60 за 2 кв. 2026.xlsx"}}] ``` @@ -150,18 +173,19 @@ uv run jira-hub agent publish ../jira-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 /issues` | клиент | фильтры `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`, `hide_closed` (`1` — без задач в статусах из настройки `closed_statuses`, по умолчанию «Готово», «Снято»), `limit`, `offset` | +| `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` | +| `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}` | клиент | задача целиком | -| `GET /board` | клиент | снимок для доски: все задачи с `fact_status`/`work_kind`, все связи, `last_sync` — последний успешный полный прогон | +| `GET /board` | клиент | `?project=<метка>` (неизвестная — 404): снимок для доски — задачи проекта и соседи по связям, с `fact_status`/`work_kind`, все связи, `last_sync` — последний успешный полный прогон | | `GET /`, `GET /links` | — | перенаправление на `/app/board` | | `PUT /issues/{key}/note` | клиент | заметки (частично) | -| `POST /batches`, `GET /batches`, `GET /batches/{id}` | клиент | пакеты команд | +| `POST /batches`, `GET /batches`, `GET /batches/{id}` | клиент | пакеты команд; `project` в теле `POST /batches` — пакет проекта (проверки метки, ограничение задач проектом), `?project=` у `GET /batches` — только пакеты проекта | | `POST /batches/{id}/cancel` | клиент | отмена пакета | -| `POST /commands/{id}/retry`, `POST /commands/{id}/cancel` | клиент | повтор (новый пакет) / отмена (`force` для взятой) | -| `POST /refresh` | клиент | `{keys}`, `{jql}` или `{full: true}` | +| `POST /commands/{id}/retry`, `POST /commands/{id}/cancel` | клиент | повтор (новый пакет; `project` в теле — отказ, если команда не из этого проекта) / отмена (`force` для взятой) | +| `POST /refresh` | клиент | `{keys}`, `{jql}` или `{full: true}` (общая JQL по меткам всех проектов; проектов нет — 422 «нет проектов») | | `POST /files` | клиент | загрузить файл вложения `{content: base64}` → `{file: sha256, size}`; не base64 — 400, пустой — 422 `empty_file`, больше 10 МБ (10 485 760 байт) — 413 `too_large`; повтор того же содержимого — тот же `file` | | `GET /agent`, `GET /sync-runs` | клиент | состояние агента (с `release`, `update_state`, `launcher_version`, `agent_reported`, `update_hint`), журнал загрузок | -| `GET /settings`, `PUT /settings` | клиент | настройки (PUT частично, поднимает `settings_version`; ответ — все настройки после записи, как у GET) | +| `GET /settings`, `PUT /settings` | клиент | настройки (PUT частично, поднимает `settings_version`; ответ — все настройки после записи, как у GET); `base_jql` вычисляется из проектов (без проектов — `null`), запись `base_jql` и `board_id` — 422 | ```bash TOKEN=$(uv run jira-hub tokens | sed -n 's/^клиентский токен: //p') @@ -170,7 +194,7 @@ curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8790/issues?sprint= ## Веб-приложение -Адреса: `http://127.0.0.1:8790/app/` — доска `/app/board`, задачи `/app/issues`, карточка `/app/issues/{key}` — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); `/` и +Адреса: `http://127.0.0.1:8790/app/` — доска `/app/board?project=<метка>`, задачи `/app/issues?project=<метка>`, карточка `/app/issues/{key}` — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); `/` и `/links` перенаправляют на доску. Вход по клиентскому токену (`uv run jira-hub tokens`), токен хранится в браузере; `localhost` и `127.0.0.1` — разные сайты для браузера, токен и раскладка у каждого свои. Выкатка на Mac mini: @@ -181,6 +205,19 @@ make web launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub ``` +Проект выбирается выпадающим списком «Проект» в верхней панели (метка и название); пункт «+ Новый проект…» открывает окно +(метка, название, необязательно номер доски Jira): после создания приложение переходит в новый проект и показывает ход +пакета обновления. Текущий проект — параметр `project` в адресе; без него приложение подставляет последний выбранный +(он помнится в браузере) или первый из списка; метка, которой нет в хабе, — «проект <метка> не найден». Раскладка и +флаги доски хранятся отдельно для каждого проекта (у AWPCA — прежние ключи `localStorage`, у остальных с суффиксом `@<метка>`). + +Выкатка проектов (порядок обязателен): агент v12 опубликован и `agent_status` показывает v12 → слияние хаба, `make web`, +перезапуск хаба (`launchctl kickstart`, только владелец; миграция схемы v4 заводит проект AWPCA «АРМ КА») → +`min_agent_version = 12` → в репозиторий `awp-ca` положить `.mcp.json` с `--project AWPCA` и переподключить MCP. Хаб +новее агента v11 не выкатывать до первого шага: v11 не знает доски проекта в параметрах команд. Доску проекта +исправляет только владелец, прямо в базе хаба: `UPDATE project SET board_id = <номер>, board_type = NULL WHERE label = '<метка>'` +(тип доски определит следующий `refresh`). + Без `make web` хаб на `/app…` отвечает 503 с подсказкой. Шрифты (IBM Plex) входят в сборку: приложение работает без доступа в интернет. Раскладка доски и вход сохраняются: приложение читает те же ключи `localStorage`, что прежняя страница. Разработка: `bash web/scripts/test-hub.sh` (тестовый хаб @@ -206,9 +243,9 @@ launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub | `ensure_comment` | `key`, `marker`, `body` | | `ensure_status` | `key`, `target` (имя статуса или список-путь) | | `ensure_link` | `source`, `target`, `link_type`, `replace` | -| `ensure_sprint` | `key`, `sprint` (имя или `backlog`) | +| `ensure_sprint` | `key`, `sprint` (имя или `backlog`); доску проекта задачи (`board_id`) дописывает хаб, от клиента параметр отвергается | | `ensure_attachment` | `key` (ключ или `new:`), `file` (sha256 из `POST /files`), `filename` (имя вложения в Jira, без `/`, `\`, `"` и управляющих символов); `size` хаб дописывает при выдаче агенту | -| `refresh` | `keys` или `jql` | +| `refresh` | `keys` или `jql`; при выдаче агенту хаб дописывает `boards` — доски проектов | Тип `bug` — «Ошибка» в Jira, обязательные поля те же, что у задачи. Ставится, только если в настройках есть `issue_types.bug` с непустым `name` или `id`, иначе 422 «команда N: нет настройки issue_types.bug». `init` берёт его из diff --git a/openspec/changes/projects/design.md b/openspec/changes/projects/design.md index ea9be1c..87b28ab 100644 --- a/openspec/changes/projects/design.md +++ b/openspec/changes/projects/design.md @@ -303,3 +303,44 @@ 35. 2026-10-19 — **в тесте `storage.test.ts` выражение `{…} as JsonImport` заменено на `as unknown as JsonImport`.** Обоснование: `tsc --noEmit` (шаг `make check`) отвергает прямое приведение частичного объекта к `JsonImport` (нет `model`, `dateColumns`, `collapsedLanes`); в тесте важны только `layout`, `layoutByDate`, `laneOrder`, `filters`, и `applyJsonSettings` читает только их. Цена ошибки: тест передаёт неполный импорт; если `applyJsonSettings` начнёт читать другие поля, тест выдаст неверный результат, а не ошибку типа. 36. 2026-10-19 — **`POST /batches` с неизвестным `project`: сообщение «проекта {P} нет» без префикса «команда {i}».** Обоснование: ошибка пакетная, а не одной команды. Цена ошибки: только текст сообщения. +37. 2026-10-19 — **`GET /projects` отвечает `{"projects": [...]}`.** Обоснование: как прочие списки API + (`{"batches": …}`), ответ расширяем полями без поломки клиентов. Цена ошибки: форма ответа; клиент `client` и веб + читают ключ `projects`. +38. 2026-10-19 — **пакет «Новый проект» — с `batch.project = NULL`.** Обоснование: его `refresh` по общей JQL обновляет все + проекты, а не один; привязка к новой метке спрятала бы его из общих списков и от проверок других проектов. Цена + ошибки: пакет виден в `GET /batches` без фильтра и не виден с `?project=`. +39. 2026-10-19 — **доска для `ensure_sprint` выбирается и в пакетах без проекта (веб-приложение, старые клиенты); задача вне + всех проектов — 422, хотя раньше проходила.** Обоснование: без доски проекта агенту нечего передать, прежней общей + доски в настройках больше нет (решение 4). Цена ошибки: постановка спринта задаче без метки проекта отказывает с + «не входит ни в один проект». +40. 2026-10-19 — **`board_id` от клиента в `ensure_sprint` отвергается общей проверкой «лишние параметры board_id», без + отдельного текста.** Обоснование: параметр ставит только хаб; общая проверка уже называет допустимые параметры. Цена + ошибки: текст отказа не объясняет, что доску определяет проект. +41. 2026-10-19 — **веб: память выбранного проекта — `localStorage["hub-project"]`; ручная смена проекта — новая запись + истории, фильтры списка сбрасываются; автоподстановка проекта в адрес — `replace` и только без открытой карточки.** + Обоснование: «Назад» должен возвращать к прежнему проекту; подстановка не должна заменять прямой адрес карточки. + Цена ошибки: один ключ `localStorage` и ветка в подстановке адреса. +42. 2026-10-19 — **e2e: пакет «Новый проект» (автор `viewer`) в `projects.spec.ts` разбирается `agentServeRefresh()`.** + Обоснование: иначе он остаётся в очереди и держит «Обновить из Jira» в других файлах на общем тестовом хабе. Цена + ошибки: нестабильные e2e соседних файлов при смене порядка файлов. +43. 2026-10-19 — **миграция v3 → v4 — Python-шаг `projects.migrate_v3`, вызываемый из `db.MIGRATIONS[3]` с ленивым + импортом; кавычки вокруг метки в старой `base_jql` снимаются; JQL с хвостом (`ORDER BY` и т. п.) не разбирается — + проектов нет, предупреждение в лог.** Обоснование: чистый SQL метки из JQL не вынимает; `projects` зависит от `db`, а + не наоборот, поэтому импорт ленивый. Цена ошибки: при нестандартной `base_jql` проекты придётся завести вручную + (`POST /projects`). +44. 2026-10-19 — **`base_jql` убран из `AGENT_KEYS` (агент его не читает: JQL приходит в `params.jql` команды).** + Обоснование: ключ в настройках агента вводил бы в заблуждение и расходился с вычисляемым значением. Цена ошибки: + старый агент без ключа — проверено, он берёт JQL из команды. +45. 2026-10-19 — **метка в `?project=`, `POST /batches {project}`, `retry {project}`, `--project` сравнивается точно + (регистр значим).** Обоснование: метка хранится как введена, принадлежность задачи — точное совпадение (общие + правила); неточная привязка размыла бы разрез. Цена ошибки: `--project awpca` отвечает «проект не заведён». +46. 2026-10-19 — **веб: набор ключей доски — состояние модуля `storage.ts`, выставляется в `useBoardPrefs` при монтировании + страницы (страница пересоздаётся по `key={project}`); миграции старых ключей — только для AWPCA.** Обоснование: + функции хранилища вызываются из множества мест без параметра проекта; пересоздание страницы исключает гонку ключей. + Цена ошибки: глобальное состояние модуля; чтение ключей до монтирования страницы даст набор прежнего проекта. +47. 2026-10-19 — **тестовый хаб: `seed-hub.py` дописывает метку `AWPCA` задачам фикстуры без `neighbor`; файл фикстуры не + меняется.** Обоснование: в `hub-basic` метка есть только у двух задач из десяти, а разрез доски — по метке; golden- + эталоны привязаны к фикстуре. Цена ошибки: e2e видят задачи, которых по меткам фикстуры в проекте нет. +48. 2026-10-19 — **эталон `hub-empty.expected.json` не перегенерируется (его пишет прежняя страница, где `hasSprint` всегда + `true`): тест сравнивает с ним, подменив `hasSprint` на `false`.** Обоснование: решение 12 меняет смысл `hasSprint`; + эталон — снимок прежней страницы. Цена ошибки: эталон расходится с прежней страницей в одном поле. diff --git a/web/e2e/projects.spec.ts b/web/e2e/projects.spec.ts new file mode 100644 index 0000000..9671c8c --- /dev/null +++ b/web/e2e/projects.spec.ts @@ -0,0 +1,49 @@ +import { expect, test } from "@playwright/test"; +import { agentServeRefresh, login } from "./hub.ts"; + +// проект заводится один раз на тестовый хаб: сценарии идут по порядку, файл — последний по алфавиту +test.describe.serial("проекты", () => { + test("первый вход: проект AWPCA в адресе, на доске задача проекта", async ({ page }) => { + await login(page); + await expect(page).toHaveURL(/\/app\/board\?project=AWPCA$/); + await expect(page.locator('.board .card[data-key="AURORA-9001"]')).toBeVisible(); + }); + + test("метка занята: окно остаётся открытым, ошибка в alert", async ({ page }) => { + await login(page); + await page.getByLabel("Проект", { exact: true }).selectOption({ label: "+ Новый проект…" }); + const d = page.getByRole("dialog", { name: "Новый проект" }); + await d.getByLabel("Метка").fill("awpca"); + await d.getByLabel("Название").fill("x"); + await d.getByRole("button", { name: "Создать" }).click(); + await expect(d.getByRole("alert")).toContainText("уже есть"); + await expect(d).toBeVisible(); + }); + + test("создание проекта из формы: адрес, пакет, разбор пакета агентом", async ({ page }) => { + await login(page); + await page.getByLabel("Проект", { exact: true }).selectOption({ label: "+ Новый проект…" }); + const d = page.getByRole("dialog", { name: "Новый проект" }); + await d.getByLabel("Метка").fill("STOCKS_DRRK"); + await d.getByLabel("Название").fill("Запасы ДРРК"); + await d.getByRole("button", { name: "Создать" }).click(); + await expect(page).toHaveURL(/\?project=STOCKS_DRRK/); + await expect(page.locator(".board .card[data-key]")).toHaveCount(0); + await expect(page.getByRole("status")).toContainText(/пакет #\d+/); + await agentServeRefresh(); // иначе пакет держит «Обновить из Jira» в других файлах на общем тестовом хабе + await expect(page.getByRole("status")).toContainText("выполнен"); + }); + + test("переключение проекта и раздела", async ({ page }) => { + await login(page, "/app/board?project=STOCKS_DRRK"); + await page.getByLabel("Проект", { exact: true }).selectOption({ label: "AWPCA — АРМ КА" }); + await expect(page.locator('.board .card[data-key="AURORA-9001"]')).toBeVisible(); + await page.getByRole("link", { name: "Задачи", exact: true }).click(); + await expect(page).toHaveURL(/\/app\/issues\?project=AWPCA$/); + }); + + test("неизвестный проект в адресе", async ({ page }) => { + await login(page, "/app/issues?project=NOPE"); + await expect(page.getByText("проект NOPE не найден")).toBeVisible(); + }); +});