diff --git a/CLAUDE.md b/CLAUDE.md index 5c1b039..d9ac3e5 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/web-app/`. +Хаб моста Jira для АРМ КА: единственный источник истины по задачам проекта AURORA (метка AWPCA) на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; текущего change нет (последний — `openspec/changes/archive/2026-10-15-web-app/`). ## Стек и ограничения - Веб-приложение — `web/` (React 19 + TypeScript + Vite, TanStack Query, Vitest, Playwright; Node 24, npm); сборка в `src/jira_hub/web/app/` не в git. diff --git a/openspec/changes/web-app/.openspec.yaml b/openspec/changes/archive/2026-10-15-web-app/.openspec.yaml similarity index 100% rename from openspec/changes/web-app/.openspec.yaml rename to openspec/changes/archive/2026-10-15-web-app/.openspec.yaml diff --git a/openspec/changes/web-app/design.md b/openspec/changes/archive/2026-10-15-web-app/design.md similarity index 100% rename from openspec/changes/web-app/design.md rename to openspec/changes/archive/2026-10-15-web-app/design.md diff --git a/openspec/changes/web-app/proposal.md b/openspec/changes/archive/2026-10-15-web-app/proposal.md similarity index 100% rename from openspec/changes/web-app/proposal.md rename to openspec/changes/archive/2026-10-15-web-app/proposal.md diff --git a/openspec/changes/web-app/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-15-web-app/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/web-app/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-15-web-app/specs/hub-api/spec.md diff --git a/openspec/changes/web-app/specs/web-app/spec.md b/openspec/changes/archive/2026-10-15-web-app/specs/web-app/spec.md similarity index 100% rename from openspec/changes/web-app/specs/web-app/spec.md rename to openspec/changes/archive/2026-10-15-web-app/specs/web-app/spec.md diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index ebf66f6..3b70e91 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -9,10 +9,10 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача `jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:` (по умолчанию 8790), база и настройки в `~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты API SHALL требовать `Authorization: Bearer `: агентский токен открывает только `/agent/*`, клиентский — всё остальное. Исключение — -страница доски связей (`GET /`, `GET /links`): это статический файл без данных, он отдаётся без токена. Ответы API — -JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или `Content-Length`), 401, 404, 409, 413 (тело больше -предела), 422, 500 (внутренняя ошибка); реле общей папки дополнительно отвечает 502. Методы, кроме GET, POST и PUT, -отвечают 501 стандартной страницей `http.server`, не JSON. +раздача веб-приложения (`GET /app`, `GET /app/…`) и перенаправления `GET /`, `GET /links`: это статические файлы без +данных, они отдаются без токена. Ответы API — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или +`Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки +дополнительно отвечает 502. Методы, кроме GET, POST и PUT, отвечают 501 стандартной страницей `http.server`, не JSON. #### Scenario: Чужой токен - **WHEN** агентским токеном вызван `GET /issues` @@ -23,8 +23,8 @@ JSON, ошибки — `{error, detail}` с кодами 400 (неверное - **THEN** 501, тело не JSON; база не меняется #### Scenario: Страница без токена -- **WHEN** `GET /` без заголовка `Authorization` -- **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — страница доски связей; данных зеркала в ней нет +- **WHEN** `GET /app/board` без заголовка `Authorization`, сборка приложения есть +- **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — `index.html` сборки; данных зеркала в нём нет ### Requirement: Маршруты агента Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном): @@ -234,45 +234,6 @@ JSON, ошибки — `{error, detail}` с кодами 400 (неверное - **WHEN** `GET /board` без токена или с агентским токеном - **THEN** 401 -### Requirement: Страница доски связей -Хаб SHALL отдавать по `GET /` и `GET /links` страницу доски связей — один HTML-файл из пакета хаба -(`src/jira_hub/web/links.html`, развитие просмотрщика «Спринт 2 линк»). Страница SHALL: -- спрашивать клиентский токен хаба при первом открытии и после ответа 401, хранить его в `localStorage` браузера и - передавать в заголовке `Authorization` своих запросов к API того же хоста; -- при открытии загружать `GET /board` и строить доску из него: задачи-эпики — дорожки, остальные задачи (включая - соседей) — карточки; вид работы — по меткам `kind-*`, иначе по типу задачи; срок карточки — `planned_end`, при его - отсутствии `duedate`; связи — по известным странице типам Jira, неизвестные типы пропускаются; -- показывать в шапке время последнего полного прогона (`last_sync.finished_at`); -- по кнопке «Обновить из Jira» ставить `POST /refresh {full: true, author: "viewer"}`, показывать ход пакета по - `GET /batches/{id}` до завершения и затем перечитывать `GET /board`; если агент офлайн (`GET /agent`), предупреждать, - но пакет ставить; при ошибке пакета — показывать её текст; -- по кнопке «Перечитать» загружать `GET /board` заново без обращения к Jira; -- сохранять раскладку карточек, порядок и свёртку дорожек, фильтры в `localStorage` под ключами, не зависящими от - источника данных; импорт JSON, выгруженного страницей, в режиме хаба SHALL сохранять эти настройки и сразу - перечитывать данные хаба, а не показывать снимок из файла; -- оставлять загрузку CSV из фильтра Jira как запасной путь (снимок с пометкой «из файла»; «Перечитать» возвращает данные - хаба); -- при недоступном хабе показывать понятное сообщение и предлагать загрузить файл; встроенной выгрузки данных страница - не содержит. - -#### Scenario: Первый вход -- **WHEN** страница открыта впервые, токена в `localStorage` нет -- **THEN** вместо доски — поле клиентского токена с подсказкой `uv run jira-hub tokens`; после ввода верного токена доска - строится из `GET /board`, токен сохранён - -#### Scenario: Неверный токен -- **WHEN** сохранённый токен устарел и `GET /board` ответил 401 -- **THEN** страница снова показывает поле токена с пояснением, что токен не подошёл - -#### Scenario: Обновление из Jira -- **WHEN** нажата «Обновить из Jira», агент на связи -- **THEN** в хабе пакет из одного `refresh` по базовой JQL с автором `viewer`; страница показывает «пакет #N выполняется», - по завершении перечитывает доску и показывает новое время данных - -#### Scenario: Перенос раскладки -- **WHEN** в режиме хаба импортирован JSON, выгруженный старой страницей «Спринт 2 линк» -- **THEN** раскладка, дорожки и фильтры из JSON применены к задачам хаба; задачи и связи — из хаба, не из JSON - ### Requirement: Публикация версии агента `jira-hub agent publish PATH` SHALL публиковать файл агента: текст в UTF-8 не больше 1 МБ, ровно одна строка `AGENT_VERSION = <целое>`, версия больше опубликованной. Файл SHALL записываться в `/release/agent.py` @@ -300,3 +261,37 @@ sha256 <первые 12 символов>…, байт»; при ошиб #### Scenario: Агент без запускателя - **WHEN** опубликована v6, агент v5 онлайн, в последнем `claim` `launcher_version = null` - **THEN** `GET /agent` → `update_hint` = «агент запущен без run.py — обновится только вручную» + +### Requirement: Раздача веб-приложения +Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/jira_hub/web/app/`, читая файлы +с диска на каждый запрос: +- `GET /app/assets/<путь>` — файл из `assets/` сборки; путь, который после разрешения выходит за `assets/` или не + существует, — 404; `Content-Type` по расширению (`.js` — `text/javascript; charset=utf-8`, `.css` — + `text/css; charset=utf-8`, `.svg` — `image/svg+xml`, `.png` — `image/png`, `.ico` — `image/x-icon`, `.woff2` — + `font/woff2`, `.map` — `application/json`, прочее — `application/octet-stream`); + `Cache-Control: public, max-age=31536000, immutable`; +- любой другой `GET /app` или `GET /app/…` — `index.html` сборки, `text/html; charset=utf-8`, `Cache-Control: no-cache`; +- если `index.html` сборки нет — на любой `GET /app…` 503 `text/html; charset=utf-8` с текстом, что приложение не + собрано и нужно выполнить `make web` в каталоге jira-hub; +- `GET /` и `GET /links` — 302 с `Location: /app/board`. +Сборка приложения не хранится в git; её создаёт `make web`. + +#### Scenario: Файл сборки +- **WHEN** в сборке есть `assets/index-abc123.js`, запрошен `GET /app/assets/index-abc123.js` +- **THEN** 200, `Content-Type: text/javascript; charset=utf-8`, `Cache-Control` с `immutable`, тело — содержимое файла + +#### Scenario: Путь приложения +- **WHEN** запрошен `GET /app/issues/AURORA-1620` +- **THEN** 200, тело — `index.html` сборки, `Cache-Control: no-cache` + +#### Scenario: Выход за каталог сборки +- **WHEN** запрошен `GET /app/assets/../../api.py` (в том числе в виде `%2e%2e`) +- **THEN** 404, содержимое файлов вне `assets/` не отдаётся + +#### Scenario: Приложение не собрано +- **WHEN** каталога сборки нет, запрошен `GET /app/board` +- **THEN** 503, тело — HTML с подсказкой `make web` + +#### Scenario: Старый адрес доски +- **WHEN** запрошен `GET /links` или `GET /` +- **THEN** 302, `Location: /app/board` diff --git a/openspec/specs/web-app/spec.md b/openspec/specs/web-app/spec.md new file mode 100644 index 0000000..1aebfb1 --- /dev/null +++ b/openspec/specs/web-app/spec.md @@ -0,0 +1,170 @@ +# web-app Specification + +## Purpose +Веб-приложение хаба (`web/`, React + TypeScript + Vite) под префиксом `/app/`: вход по клиентскому токену, доска связей (перенос `links.html`) и страницы задач со служебными заметками; сборка в `src/jira_hub/web/app/`. + +## Requirements + +### Requirement: Вход по токену +Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в `localStorage` под ключом +`jira-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса. +Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с +подсказкой `uv run jira-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода. +Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен» +SHALL очищать токен и показывать экран входа. + +#### Scenario: Первый вход +- **WHEN** приложение открыто впервые, токена нет +- **THEN** показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён + +#### Scenario: Устаревший токен +- **WHEN** сохранённый токен не подходит и запрос ответил 401 +- **THEN** показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны + +#### Scenario: Токен от прежней доски +- **WHEN** в `localStorage` есть `jira-hub-token`, сохранённый страницей `links.html` +- **THEN** приложение входит с ним без запроса токена + +### Requirement: Оболочка и адреса +Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список +задач) и `/app/issues/{key}` (карточка задачи); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь — +на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать переходы «Доска», «Задачи» и действие +«Сменить токен». Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не +пустым экраном. + +#### Scenario: Переход между разделами +- **WHEN** на доске выбран раздел «Задачи» +- **THEN** адрес — `/app/issues`, показан список задач; «назад» возвращает на доску + +### Requirement: Ошибки хаба +Приложение SHALL различать ошибки запросов к хабу: 401 — экран входа; хаб не отвечает (сеть, 5xx) — сообщение +«хаб не отвечает» с возможностью повторить, ранее загруженные данные остаются на экране; прочие 4xx — текст `detail` +ответа рядом с действием, которое его вызвало. + +#### Scenario: Хаб остановлен +- **WHEN** доска показана, хаб остановлен, нажато «Перечитать» +- **THEN** на доске прежние данные и сообщение «хаб не отвечает — показаны прежние данные» + +### Requirement: Справочники из настроек хаба +Приложение SHALL брать перевод меток видов работ (`kind_labels`) и типов связей Jira (`link_types`) из `GET /settings` +и не содержать собственных копий этих словарей. Если нужного словаря в настройках нет, страница SHALL сообщать, какого +словаря не хватает, вместо пустой доски или списка без подписей. + +#### Scenario: Новый тип связи в настройках +- **WHEN** в `link_types` настроек добавлен тип и в зеркале есть связь этого типа +- **THEN** после перезагрузки страницы доска показывает связь этого типа без изменения кода приложения + +### Requirement: Доска связей +Страница `/app/board` SHALL воспроизводить поведение доски связей хаба (`links.html` ревизии 3931a88) без изменений, +кроме перечисленных в этом требовании и в требовании «Источник данных доски»: построение модели из `GET /board`, CSV +фильтра Jira и JSON доски (`arm-ka-links/1`); дорожки по эпикам со свёрткой и перестановкой, служебные дорожки внизу; +карточки с перетаскиванием, растяжкой, размером по содержимому и привязкой к сетке; стрелки связей с выравниванием; +фильтры по спринтам и исполнителям; легенда со скрытием видов работ и типов связей; «+ соседи по связям», «⇄ передачи», +«скрыть без связей», «⚑ критический путь», «▥ по датам» с колонками дат и их растяжкой; ящик «Проблемы»; таблица +связей; экспорт и импорт JSON; тема; «Сбросить раскладку». Раскладка, дорожки, фильтры и тема SHALL храниться в +`localStorage` под теми же ключами `graph-<имя>…`, что у `links.html`, с той же миграцией старых ключей. +Результат построения модели, видимой выборки, критического пути и проблем SHALL совпадать с результатом `links.html` +на одних и тех же данных и фильтрах. У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку», +ведущий на `/app/issues/{key}`; ключ карточки по-прежнему открывает задачу в Jira. Действия «Сменить токен» в меню +доски нет — оно в верхней панели приложения. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку +карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке +захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет. + +#### Scenario: Раскладка переживает переход +- **WHEN** раскладка карточек сохранена страницей `links.html`, открыта `/app/board` с теми же данными +- **THEN** карточки стоят на тех же местах, дорожки в том же порядке и свёртке, фильтры те же + +#### Scenario: Совпадение со старой доской +- **WHEN** на одной фикстуре `GET /board` и одном наборе фильтров построены модель, видимая выборка, критический путь + и проблемы старой страницей и приложением +- **THEN** результаты совпадают + +#### Scenario: Открыть карточку +- **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку» +- **THEN** открыта `/app/issues/AURORA-1620` внутри приложения + +### Requirement: Источник данных доски +Доска SHALL хранить явный источник данных — хаб или файл. Пока показан снимок из файла (CSV или JSON-снимок), ответы +хаба SHALL его не заменять; «Перечитать» SHALL возвращать источник «хаб» и загружать `GET /board` заново без обращения к +Jira. Импорт JSON в режиме хаба SHALL применять раскладку, дорожки и фильтры и перечитывать хаб. «Обновить из Jira» +SHALL проверять агента (`GET /agent`; офлайн — предупреждение, пакет всё равно ставится), ставить +`POST /refresh {full: true, author: "viewer"}`, опрашивать `GET /batches/{id}` каждые 3 с одним опросом до +терминального статуса пакета и затем перечитывать `GET /board`. Кнопка «Обновить из Jira» SHALL быть недоступна, +пока запрос `/refresh` в пути или пакет выполняется. Незавершённый пакет `refresh` с автором `viewer` SHALL +подхватываться при каждой успешной загрузке из хаба. Опрос пакета SHALL прекращаться при ответе 404 и после 5 ошибок +«хаб не отвечает» подряд, с сообщением и снова доступной кнопкой. Шапка доски SHALL показывать +источник данных (данные хаба на время последнего полного прогона; «из файла») и одно текущее событие (загрузка; пакет +выполняется / выполнен / ошибка с текстом; хаб не отвечает — прежние данные; агент офлайн — пакет поставлен; ошибка +разбора файла — «файл не разобран: <причина>»); новое событие заменяет предыдущее целиком. + +#### Scenario: Поздний ответ хаба не затирает файл +- **WHEN** запрос `GET /board` в пути, и в это время загружен CSV +- **THEN** после ответа хаба на доске по-прежнему данные CSV с пометкой «из файла» + +#### Scenario: Нет второго полного прогона +- **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, доска перерисована (фильтр, «Перечитать») +- **THEN** кнопка остаётся недоступной, в хабе один пакет, его опрашивает один опрос + +#### Scenario: Пакет пропал +- **WHEN** опрос пакета получил 404 +- **THEN** опрос остановлен, показано сообщение, кнопка «Обновить из Jira» снова доступна + +#### Scenario: Пакет после «Перечитать» +- **WHEN** пакет `viewer` поставлен с другой вкладки и не завершён, нажато «Перечитать» +- **THEN** доска показывает «пакет #N выполняется» и по завершении перечитывает данные + +### Requirement: Список задач +Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (ссылка на карточку), тема, +тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы, +статус по факту, признак «сосед». Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид +работы, статус по факту, статус Jira, метка, эпик, только соседи, поиск по ключу или теме), храниться в query-строке +адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`. Задачи SHALL +загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя порция полная. + +#### Scenario: Фильтр в адресе +- **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель +- **THEN** адрес содержит `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого + адреса в новой вкладке показывает ту же выборку + +#### Scenario: Конец списка +- **WHEN** вторая порция вернула 17 задач +- **THEN** в таблице 67 задач, «Показать ещё» не показана + +### Requirement: Карточка задачи +Страница `/app/issues/{key}` SHALL показывать задачу из `GET /issues/{key}`: поля зеркала, ссылку на задачу в Jira, +описание как текст с сохранёнными переносами строк (без разбора разметки Jira), исходящие и входящие связи с подписью +типа (логический тип из `link_types`, иначе имя типа Jira) и ссылками на карточки связанных задач, 10 последних команд +по задаче (id, пакет, тип, статус, время завершения, ошибка). Если задачи нет в зеркале (404), страница SHALL сообщать +об этом со ссылкой на список. + +#### Scenario: Связанная задача +- **WHEN** у AURORA-1620 исходящая связь `ASUPR_Блокер_Начала` на AURORA-1621 +- **THEN** в карточке «блокирует начало → AURORA-1621», ключ ведёт на `/app/issues/AURORA-1621` + +### Requirement: Служебные заметки +Карточка задачи SHALL давать править служебные заметки: «вид работы» — выбор из значений `kind_labels` настроек +(и текущего значения, если его там нет, и пустого), «статус по факту» — текст с подсказками из ключей `status_labels`, +«сделано», «осталось», «заметка владельца» — многострочный текст. Сохранение SHALL отправлять +`PUT /issues/{key}/note` только с изменёнными полями и `updated_by: "web"`; очищенное поле отправляется как `null`. +После успеха SHALL показываться сохранённая заметка с временем и автором; при 422 — текст ошибки у формы без потери +введённого. Уход со страницы с несохранёнными правками SHALL требовать подтверждения. Заметки пишутся только в хаб, +в Jira приложение ничего не отправляет. + +#### Scenario: Только изменённые поля +- **WHEN** в заметке изменено только «осталось» и нажато «Сохранить» +- **THEN** тело `PUT /issues/{key}/note` — `{rest_text: "…", updated_by: "web"}` + +#### Scenario: Уход с несохранёнными правками +- **WHEN** в форме заметки есть несохранённая правка и выбран раздел «Доска» +- **THEN** приложение спрашивает подтверждение; при отказе остаётся в карточке с введённым текстом + +### Requirement: Сборка и разработка +Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и +`package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в +`src/jira_hub/web/app/`; каталог сборки не хранится в git. `make check` SHALL запускать `uv run pytest`, проверку типов +и линтер фронта и тесты фронта; зелёным считается только весь набор. `npm run dev` SHALL поднимать сервер разработки с +прокси маршрутов API на хаб из `JIRA_HUB_URL` (по умолчанию тестовый `http://127.0.0.1:8791`). + +#### Scenario: Сборка с нуля +- **WHEN** в чистом checkout выполнен `make web` +- **THEN** в `src/jira_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки