From 590e15e0e07ad758ef651d7b8bf97a8dd92e0bc9 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 15 Oct 2026 12:18:35 +0400 Subject: [PATCH] =?UTF-8?q?docs(openspec):=20change=20issue-card-modal=20?= =?UTF-8?q?=E2=80=94=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8=20=D0=BE=D0=BA=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC,=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20=D0=B8=D0=B7=20Jira=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../changes/issue-card-modal/.openspec.yaml | 2 + openspec/changes/issue-card-modal/design.md | 156 +++++++++++++++++ openspec/changes/issue-card-modal/proposal.md | 60 +++++++ .../issue-card-modal/specs/agent/spec.md | 36 ++++ .../issue-card-modal/specs/data-model/spec.md | 66 +++++++ .../issue-card-modal/specs/hub-api/spec.md | 125 ++++++++++++++ .../issue-card-modal/specs/mcp/spec.md | 53 ++++++ .../issue-card-modal/specs/web-app/spec.md | 162 ++++++++++++++++++ 8 files changed, 660 insertions(+) create mode 100644 openspec/changes/issue-card-modal/.openspec.yaml create mode 100644 openspec/changes/issue-card-modal/design.md create mode 100644 openspec/changes/issue-card-modal/proposal.md create mode 100644 openspec/changes/issue-card-modal/specs/agent/spec.md create mode 100644 openspec/changes/issue-card-modal/specs/data-model/spec.md create mode 100644 openspec/changes/issue-card-modal/specs/hub-api/spec.md create mode 100644 openspec/changes/issue-card-modal/specs/mcp/spec.md create mode 100644 openspec/changes/issue-card-modal/specs/web-app/spec.md diff --git a/openspec/changes/issue-card-modal/.openspec.yaml b/openspec/changes/issue-card-modal/.openspec.yaml new file mode 100644 index 0000000..a216797 --- /dev/null +++ b/openspec/changes/issue-card-modal/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-15 diff --git a/openspec/changes/issue-card-modal/design.md b/openspec/changes/issue-card-modal/design.md new file mode 100644 index 0000000..961beeb --- /dev/null +++ b/openspec/changes/issue-card-modal/design.md @@ -0,0 +1,156 @@ +## Context + +`main` 13e6c16. Устройство, на которое опираются правки: + +- **Агент** (`jira-agent/src/agent.py`, v9): `refresh` читает задачи `search` с `fields = STATE_FIELDS` (+ плановые даты и + поле эпика), `to_state` собирает словарь задачи, пачки по 100 уходят в `POST /agent/state`. Комментарии агент читает + только в `ensure_comment` (`GET /issue/{key}/comment`, постранично по 100). +- **Хаб**: `mirror.apply_state` пишет в `issue` только столбцы `ISSUE_COLUMNS` — лишние поля задачи (в том числе будущее + `comments`) молча пропускаются, поэтому агент v10 совместим с нынешним хабом. `mirror.get_issue` отдаёт задачу, связи + (`target_key`/`source_key`, `link_type`), заметку, 10 последних команд. Схема базы — v2 (`db.SCHEMA_VERSION`), + миграция с копией базы — `db.migrate`. Тело запроса ограничено 16 МБ (`api.MAX_BODY`). +- **Веб-приложение**: `createBrowserRouter(routes, {basename: "/app"})`; `Layout` с ``; маршруты `board`, + `issues`, `issues/:key` (страница `IssuePage`). Ссылки на карточку — ключ в `IssuesPage` и «ⓘ» в `board/components/ + Card.tsx`. `NoteForm` — форма заметок с `useBlocker` (смена `pathname` при правках требует подтверждения) и + `beforeunload`. `useSaveNote` после успеха обновляет кэш задачи и инвалидирует `["issues"]` и `board`. + Прокрутка доски при возврате на `/app/board` восстанавливается (M-3 #29). +- **Справочники**: `link_types` настроек — логическое имя → тип Jira (`блокирует начало` → `ASUPR_Блокер_Начала`, + `блокирует завершение` → `ASUPR_Блокер_Завершения`, `связанные` → `ASUPR_Связанные`); `status_labels` — статус по факту → + класс (`fact-done`, `fact-cancelled`, …). + +## Goals / Non-Goals + +**Goals:** карточка открывается окном поверх списка и доски без потери их состояния; по шапке видно состояние задачи, +по связям — что держит задачу и что ждёт её, с темой и статусом связанных; комментарии задачи из Jira в карточке со +скрытием; агент v10 и хаб совместимы в любом порядке выкатки. + +**Non-Goals:** см. proposal.md. + +## Decisions + +### Агент v10 (решения агентской части — `jira-agent/openspec/changes/agent-v10-comments/design.md`) + +1. **Поле `comment` в том же `search`.** `STATE_FIELDS` получает `comment`; новых вызовов Jira на обычную задачу нет. + Jira Server отдаёт `fields.comment = {comments: [...], total, maxResults, startAt}`. +2. **Дочитывание.** Если `len(comments) < total`, агент читает комментарии этой задачи `GET /issue/{key}/comment` + (`startAt`, `maxResults = 100`) до конца и берёт их вместо частичного списка. Вызовы учитываются в темпе и 429 как + любые вызовы Jira. +3. **Форма.** `comments: [{id, author, created, updated, body}]` в порядке Jira (старые первыми): `id` — строка, + `author` — `author.displayName`, иначе `author.name`, иначе `null`; `created`/`updated` — строки Jira как есть; + `body` — текст как есть (`""`, если в Jira пусто). Поле `comments` есть у каждой задачи, прочитанной агентом v10 + (пустой список — комментариев нет). +4. **Версия и выкатка.** `AGENT_VERSION = 10`; после merge PR агента — `jira-hub agent publish src/agent.py` (пишет в + каталог живого хаба: выполняет владелец или агент по его отдельной команде). + +### Хаб + +5. **Таблица `issue_comment`** `(key, comment_id, author, created, updated, body, PRIMARY KEY (key, comment_id))`, индекс + по `key`. Схема v3; миграция v2 → v3 создаёт таблицу (копия базы `schema-v2` — существующим механизмом). +6. **Запись.** `apply_state`: для задачи с полем `comments` (список) комментарии этой задачи удаляются и вставляются + присланные — в одной транзакции с задачей; задача без поля `comments` своих комментариев не теряет (агент до v10). +7. **Проверка формы** в `h_agent_state` до записи: `comments` — список объектов; `id` — непустая строка; `body` — строка; + `author`, `created`, `updated` — строка или `null`. Нарушение — 422 `bad_state` с ключом задачи и полем, пачка не + применяется целиком (агент получает ошибку хаба в отчёте `refresh`). +8. **`GET /issues/{key}`**: добавляются `comments` (новые сверху: по `created` убыв., затем `comment_id` убыв.), + `epic_summary` (тема задачи-эпика из зеркала или `null`), у каждой связи — `summary` и `status` связанной задачи + (`LEFT JOIN issue`, `null`, если её нет в зеркале). Прежние поля и их форма не меняются. +9. **MCP `issue`**: после заметок — «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), затем + «ещё M», если их больше 5; нет комментариев — «комментариев нет». + +### Веб-приложение: окно + +10. **Фоновая страница.** Ссылки на карточку (ключ в списке, «ⓘ» на доске, связи в карточке) передают в + `location.state` `{background, depth}`: `background` — адрес страницы под окном (`pathname + search`), `depth` — + сколько карточек открыто поверх неё (1 при открытии со страницы, +1 при переходе по связи). `Layout` отрисовывает + маршрут `background` (если он есть), а при адресе `/issues/:key` — окно поверх него. Страница под окном не + пересоздаётся: её состояние (фильтры, загруженные порции, прокрутка, раскладка доски, опрос пакета) сохраняется. +11. **Прямой адрес** `/app/issues/{key}` без `state` (новая вкладка, закладка, перезагрузка): под окном — список задач без + фильтров, `depth = 0`. +12. **Закрытие** (✕, Esc, щелчок по фону): при `depth ≥ 1` — `navigate(-depth)`, браузер возвращается к фоновой + странице, и «назад» после закрытия ведёт на шаг раньше неё; при `depth = 0` — `navigate("/issues", {replace: true})`. +13. **Элемент ``** с `showModal()`: фокус удерживается в окне, фон недоступен, Esc вызывает `cancel` (обрабатываем + сами — закрытие по п. 12 с проверкой заметок). Щелчок по фону — щелчок по самому `` вне области содержимого. + Прокрутка страницы под окном на время открытия блокируется (`overflow: hidden` у `body`), содержимое окна + прокручивается внутри, шапка окна закреплена. Ширина до 1000 px, высота до 90vh; на экранах уже 700 px — на всю + ширину. +14. **Несохранённые заметки.** Подтверждение «Есть несохранённые правки заметки. Уйти без сохранения?» при закрытии окна, + переходе по связи и уходе в раздел; условие блокировки — смена `pathname` при правках (как сейчас), закрытие по + п. 12 проходит через тот же блокировщик. Отказ — окно остаётся с введённым текстом. +15. **Доска под окном.** Сохранение заметки уже инвалидирует `board` и `["issues"]` (`useSaveNote`): фоновая страница + смонтирована, поэтому перечитывается и показывает новые «вид работы» и «статус по факту». Отдельного кода нет — + поведение закрепляется тестом. + +### Веб-приложение: раскладка карточки + +16. **Шапка**: ключ; статус Jira (плашка); статус по факту (плашка цветом класса из `status_labels`, если есть); + «Открыть в Jira ↗»; ✕; тема; строка «исполнитель · срок · спринт · эпик: <тема эпика>» — пустые части пропускаются, + эпик без темы в зеркале — ключом. Срок — `planned_end`, иначе `duedate`; «просрочено» (красным) — срок раньше + сегодняшней даты и статус по факту не относится к классам `fact-done`, `fact-cancelled`. +17. **Связи по смыслу.** По `link_types` настроек: входящие `блокирует начало` / `блокирует завершение` — группа «Зависит + от», исходящие — «Блокирует»; у строки метка «начало» / «завершение». `связанные` в обе стороны — «Связана с». + Тип, которого нет в `link_types`, — «Прочие» с именем типа Jira и направлением («→» исходящая, «←» входящая). + Строка: ключ (ссылка — открывает связанную задачу в окне), тема, статус Jira (плашка); связанной задачи нет в + зеркале — ключ и «нет в зеркале». Пустая группа не показывается; связей нет — «связей нет». Порядок групп: «Зависит + от», «Блокирует», «Связана с», «Прочие»; внутри — по ключу. +18. **Описание** — `pre-wrap` без разбора разметки; выше 8 строк сворачивается с кнопкой «развернуть» / «свернуть» + (кнопка только если текст не помещается). Нет описания — раздела нет. +19. **Комментарии** — «Комментарии (N)» с кнопкой «Скрыть» / «Показать»; новые сверху; автор, время (`fmtStamp`), текст + `pre-wrap`. Выбор «скрыто» — один на все карточки, в `localStorage` `jira-hub-comments-hidden` (`"1"` — скрыты); + недоступный `localStorage` — показаны. Нет комментариев — «комментариев нет» (кнопки нет). +20. **Заметки** — по умолчанию режим чтения: «Вид работы», «Статус по факту», «Сделано», «Осталось», «Заметка владельца» + (пустые — «—»), строка «изменено <время>, <автор>»; кнопка «Править» показывает нынешнюю форму `NoteForm` + (поля, «Сохранить», «Отмена»); после успешного сохранения — снова режим чтения; «Отмена» без правок — сразу, с + правками — подтверждение «Отменить правки заметки?». Заметки ещё нет — режим чтения с «—» и «Править». +21. **Прочие поля**: тип, приоритет, автор, метки, плановое начало, обновлена в Jira, «сосед по связям» — непустые. +22. **Команды моста (N)** — свёрнуты (`
`), внутри нынешняя таблица; команд нет — «команд по задаче не было» и + раздел не сворачивается. +23. **Колонки**: под шапкой слева — «Связи», «Описание», «Комментарии»; справа (≈320 px) «Заметки», «Прочие + поля», «Команды моста». Уже 900 px — одна колонка: заметки, связи, описание, комментарии, прочие поля, команды. +24. **Состояния окна**: загрузка — «загрузка …»; 404 — «Задачи нет в зеркале хаба.» и «Закрыть»; хаб не + отвечает — сообщение и «Повторить», ранее загруженные данные остаются; поверх ошибки при данных — предупреждение + в шапке окна. + +### Проверки + +25. **Тестовый хаб**: фикстура `hub-basic` дополняется у одной задачи описанием длиннее 8 строк, тремя комментариями и + двумя командами; у второй — входящими и исходящими связями всех групп, включая тип вне `link_types` и связь на + задачу вне зеркала. Эталоны доски от этого не меняются (комментарии и описание в модель доски не входят) — проверяется + golden-тестом. +26. **e2e** (Playwright на :8791): открытие из списка (фильтр и прокрутка списка после закрытия на месте), по «ⓘ» с доски + (прокрутка и раскладка доски на месте), переход по связи и «назад», Esc / ✕ / фон, «назад» после закрытия не + открывает карточку, прямой адрес и перезагрузка с открытым окном, подтверждение при несохранённой заметке, ширина + 600 px, «Скрыть» комментарии переживает перезагрузку. + +## Risks / Trade-offs + +- **Фоновая страница в `Layout`** — нестандартный для data router приём (две отрисовки маршрутов). Риск: `useBlocker` + только один на страницу — блокировщик заметок остаётся в окне, фоновая страница блокировщиков не имеет. Проверяется + тестами п. 14 и e2e. +- **`navigate(-depth)`** полагается на то, что все `depth` шагов истории — наши переходы. Ручное изменение адреса внутри + окна сбрасывает `state` → `depth = 0`, закрытие ведёт в список задач (`replace`). Принято. +- **Объём состояния**: комментарии увеличивают пачку `POST /agent/state`; при 100 задачах и типичных комментариях это + сотни килобайт при пределе 16 МБ. Принято без разбиения. +- **Просрочка по статусу по факту** (п. 16): задача, закрытая в Jira без заметки, покажется просроченной. Цена — лишняя + красная пометка; списка «закрытых» статусов Jira в настройках нет. + +## Migration Plan + +1. PR jira-agent (v10) → merge → `jira-hub agent publish` → `agent_status` «обновление: ok v9 → v10». +2. PR jira-hub (#37) → merge → `git pull --ff-only`, `make web` в основном checkout → перезапуск хаба владельцем + (`launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`): миграция v2 → v3 с копией базы → `/mcp` reconnect. +3. Ближайший `refresh` (час или «Обновить из Jira») заполняет комментарии. +Откат хаба: копия `backups/<…>-schema-v2.sqlite` и предыдущий коммит; агент v10 со старым хабом работает (п. Context). + +## Open Questions + +Нет. + +## Decisions made without discussion + +| Маркер | Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---|---| +| R-1 | 2026-10-15 | Требования к агенту v10 — дельтой `specs/agent` в этом change jira-hub; в jira-agent — change `agent-v10-comments` только с решениями агентской части (design.md) | Заведённый порядок: спека агента живёт в `jira-hub/openspec/specs/agent/`, так сделаны v9 (hub#30 + agent#24) и предыдущие; «один change на репозиторий» соблюдается — у агента свой change и свой PR | Низкая: перенос дельты в другой change — правка документов | +| R-2 | 2026-10-15 | Просрочка — срок раньше сегодня и статус по факту не `fact-done`/`fact-cancelled`; статус Jira не учитывается | Списка «закрытых» статусов Jira в настройках нет, а свой словарь в приложении запрещён требованием «Справочники из настроек хаба» | Лишняя красная пометка у закрытой в Jira задачи без заметки | +| R-3 | 2026-10-15 | В карточке показываются все комментарии задачи, без постраничности; в MCP — 5 последних | В задачах проекта комментариев единицы; окно прокручивается внутри | При десятках длинных комментариев — длинная прокрутка окна; лечится «Скрыть» | +| R-4 | 2026-10-15 | «Команды моста» свёрнуты всегда при открытии, выбор не запоминается; «Скрыть» комментарии запоминается | Команды нужны редко и по конкретному поводу; комментарии владелец просил скрывать — это предпочтение | Лишний щелчок, если команды смотрят часто | +| R-5 | 2026-10-15 | Отмена правки заметки — кнопка «Отмена» с подтверждением при правках (в разделе 1 дизайна был только «Править»/«Сохранить») | Из режима правки нужен выход без сохранения, не закрывая окно | Лишний вопрос при отмене | diff --git a/openspec/changes/issue-card-modal/proposal.md b/openspec/changes/issue-card-modal/proposal.md new file mode 100644 index 0000000..b891327 --- /dev/null +++ b/openspec/changes/issue-card-modal/proposal.md @@ -0,0 +1,60 @@ +## Why + +Карточка задачи (`/app/issues/{key}`, change `web-app` #21) — отдельная страница: уход со списка или доски теряет +контекст, а сама карточка плохо читается — поля одного веса, связи без темы и статуса связанной задачи, входящие связи +записаны «X → тип → эта задача», техническая таблица команд на виду, форма заметок занимает пол-экрана. Комментариев +задачи из Jira в карточке нет: хаб их не хранит, агент их не читает. + +Решения владельца 15.10.2026 (brainstorming, jira-hub#37): +- карточку открывают, чтобы быстро понять состояние задачи (статус, кто, срок, что осталось) и разобраться в связях + (что держит задачу, что ждёт её); в карточке нужны комментарии задачи из Jira с возможностью их скрыть; +- карточка открывается модальным окном поверх того места, откуда её открыли (список «Задачи» или доска), у окна свой + адрес `/app/issues/{key}`; переходы по связям — внутри окна, «назад» браузера возвращает к прежней карточке; +- раскладка карточки — по разделу 1 дизайна (шапка состояния, связи по смыслу, заметки в режиме чтения, комментарии, + второстепенное внизу); визуальные макеты не нужны, раскладку предлагает агент; +- комментарии — два change по репозиториям, сначала агент: jira-agent v10 (aep/jira-agent#27) передаёт комментарии в + состоянии задачи, jira-hub (#37) хранит, отдаёт и показывает их. Требования к агенту по заведённому порядку живут в + этом change (`specs/agent`), решения агентской части — в `jira-agent/openspec/changes/agent-v10-comments/design.md`. + +## What Changes + +- **Агент v10** (`AGENT_VERSION = 10`): `refresh` запрашивает в том же `search` поле `comment`; по каждой задаче + передаёт `comments: [{id, author, created, updated, body}]` (автор — отображаемое имя, иначе логин; текст как есть); + если Jira отдала не все комментарии задачи — дочитывание через `GET /issue/{key}/comment`. Выкатка самообновлением. +- **Хаб, данные**: таблица `issue_comment`, схема базы v2 → v3 (копия базы перед миграцией — как сейчас). Присланные + комментарии задачи заменяют её прежние; задача без поля `comments` (агент до v10) свои комментарии не теряет. + Неверная форма комментария — 422 на всю пачку `POST /agent/state`. +- **Хаб, API**: `GET /issues/{key}` дополнительно отдаёт `comments` (новые сверху), у связей — `summary` и `status` + связанной задачи (`null`, если её нет в зеркале), `epic_summary`. Прежние поля не меняются. +- **MCP**: инструмент `issue` показывает 5 последних комментариев и «ещё N». +- **Веб-приложение**: + - карточка задачи — модальное окно (``) поверх списка «Задачи» или доски; адрес `/app/issues/{key}`; прямой + адрес открывает окно поверх списка задач без фильтров; закрытие (✕, Esc, щелчок по фону) возвращает туда, откуда + открыли, и «назад» после закрытия карточку не открывает; + - новая раскладка: шапка (ключ, статус Jira, статус по факту, тема, исполнитель · срок · спринт · эпик по имени, + «Открыть в Jira», ✕); связи по группам «Зависит от», «Блокирует», «Связана с», «Прочие» с темой и статусом; + описание свёрнуто до 8 строк; комментарии со «Скрыть/Показать», выбор запоминается в браузере; заметки в режиме + чтения с «Править»; «Прочие поля»; «Команды моста» свёрнуты; + - после сохранения заметки доска под окном показывает новые «вид работы» и «статус по факту». +- Спеки: `agent` — MODIFIED «Чтение состояния»; `data-model` — MODIFIED «Схема базы хаба», «Зеркало задачи»; `hub-api` — + MODIFIED «Маршруты агента», «Маршруты интерфейсов»; `mcp` — MODIFIED «Инструменты»; `web-app` — MODIFIED «Оболочка и + адреса», «Доска связей», «Список задач», «Карточка задачи», «Служебные заметки». + +## Non-Goals + +- Запись в Jira из карточки (правка полей, статуса, добавление комментариев) — следующий change `viewer-edit`. +- Разбор разметки Jira (wiki) в описании и комментариях: текст показывается как есть, с переносами строк. +- Комментарии в списке задач, на доске и в `GET /board`. +- Изменения поведения доски, кроме открытия карточки окном и обновления заметки под окном. +- Хранение истории правок комментариев: в зеркале — последнее состояние из Jira. + +## Impact + +- jira-agent: `src/agent.py` (`STATE_FIELDS`, `to_state`, дочитывание комментариев), тесты `tests/test_refresh.py`, + фейк `tests/fake_jira.py`; `AGENT_VERSION = 10`; публикация `jira-hub agent publish` после merge. +- jira-hub: `db.py` (v3, `issue_comment`), `mirror.py` (`apply_state`, `get_issue`), `api.py` (`h_agent_state` + валидация), `mcp_server.py` (`issue`), тесты; `web/src/routes.tsx`, `shell/Layout.tsx`, `issues/*` (окно, раскладка, + заметки), `board/components/Card.tsx` (ссылка «ⓘ»), `api/types.ts`, `api/queries.ts`, e2e; фикстура тестового хаба + с описанием, комментариями и командами. +- Порядок выкатки: агент v10 (любой хаб принимает его состояние — лишнее поле `comments` нынешний хаб пропускает), затем + хаб; комментарии появляются после первого `refresh` агента v10. diff --git a/openspec/changes/issue-card-modal/specs/agent/spec.md b/openspec/changes/issue-card-modal/specs/agent/spec.md new file mode 100644 index 0000000..01aaa37 --- /dev/null +++ b/openspec/changes/issue-card-modal/specs/agent/spec.md @@ -0,0 +1,36 @@ +## MODIFIED Requirements + +### Requirement: Чтение состояния +`refresh` SHALL выполняться запросом `search` (`jql`, `fields` = поля зеркала включая `issuelinks`, `comment` и спринт, +`maxResults = 100`, постранично) и отправляться хабу пачками по 100 задач со связями; ключи из `issuelinks`, которых нет +в выборке и в зеркале хаба (хаб сообщает список известных), агент SHALL дочитать одним `search` по `key in (...)` +и отправить с признаком `neighbor`. Ошибка хаба при запросе известных ключей или отправке пачки SHALL попадать в +`error` с префиксом «хаб: »; недоступность хаба и его 5xx повторяются, как обрыв связи, с источником «хаб». +Каждая задача в состоянии (с v10) SHALL нести `comments` — все комментарии задачи в порядке Jira, каждый +`{id, author, created, updated, body}`: `id` — строка; `author` — отображаемое имя автора, иначе его логин, иначе `null`; +`created`, `updated` — время Jira как есть; `body` — текст как есть (пустой — `""`). Если `search` отдал у задачи меньше +комментариев, чем `comment.total`, агент SHALL дочитать комментарии этой задачи через `GET /issue/{key}/comment` +постранично (`maxResults = 100`) и передать полный список. + +#### Scenario: Полная синхронизация +- **WHEN** `refresh {jql: "project = AURORA AND labels = AWPCA"}` при 180 задачах +- **THEN** 2 вызова `search` + 1 на соседей; `sync_run.issues_count = 180 + соседи` + +#### Scenario: Хаб не ответил при отправке состояния +- **WHEN** `refresh` через папку, реле не ответило на отправку пачки ни в одной из 4 попыток +- **THEN** отчёт `failed` с ошибкой «результат неизвестен (обрыв связи с хабом), повтор безопасен: хаб: не ответил через + папку …»; в логе повторов — «обрыв связи с хабом» + +#### Scenario: Комментарии в состоянии +- **WHEN** у AURORA-1620 в Jira два комментария, автор первого — `{name: "petrov", displayName: "Петров Пётр"}`, у автора + второго нет `displayName`, его `name` — `ivanov` +- **THEN** в состоянии AURORA-1620 `comments` из двух элементов в порядке Jira, `author` — «Петров Пётр» и «ivanov»; число + вызовов `search` не изменилось + +#### Scenario: Неполный список комментариев +- **WHEN** `search` вернул у AURORA-1620 20 комментариев при `comment.total = 130` +- **THEN** агент дочитал комментарии двумя вызовами `GET /issue/AURORA-1620/comment`; в состоянии 130 комментариев + +#### Scenario: Задача без комментариев +- **WHEN** у задачи в Jira нет комментариев +- **THEN** в её состоянии `comments = []` diff --git a/openspec/changes/issue-card-modal/specs/data-model/spec.md b/openspec/changes/issue-card-modal/specs/data-model/spec.md new file mode 100644 index 0000000..279bc7c --- /dev/null +++ b/openspec/changes/issue-card-modal/specs/data-model/spec.md @@ -0,0 +1,66 @@ +## MODIFIED Requirements + +### Requirement: Схема базы хаба +База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_comment`, `issue_note`, `batch`, +`command`, `sync_run`, `agent_status`, `setting`. Схема SHALL создаваться и мигрироваться самим хабом при старте по номеру +версии в `setting.schema_version` (текущая — 3). Миграция существующей базы SHALL выполняться в одной транзакции с +блокировкой записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в +`backups/<дата-время>-schema-v<старая>.sqlite` рядом с базой — одна копия на миграцию, сколько бы соединений ни +открывалось одновременно. Каталог данных хаба и `backups/` SHALL иметь права `0700` (в базе токены): хаб выставляет их +при создании и при каждом старте; если выставить не удалось — предупреждение в лог, работа продолжается. Денежных и +дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени) и времени +комментариев (строки Jira как есть). + +#### Scenario: Первый запуск +- **WHEN** хаб стартует с несуществующим файлом базы +- **THEN** файл создаётся, таблицы и индексы на месте, `setting.schema_version` = текущая; каталог данных с правами `0700` + +#### Scenario: Миграция базы v1 +- **WHEN** хаб стартует с базой `schema_version = 1`, в которой есть задачи, заметки и команда в `claimed` +- **THEN** в `backups/` копия базы v1; добавлена `command.deliveries` (у `claimed` — 1, у остальных — 0); добавлена пустая + `issue_comment`; данные на месте; `schema_version = 3` + +#### Scenario: Миграция базы v2 +- **WHEN** хаб стартует с базой `schema_version = 2`, в которой есть задачи, связи и заметки +- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 3` + +#### Scenario: Два соединения к базе v1 +- **WHEN** к базе `schema_version = 1` одновременно открываются два соединения +- **THEN** миграция выполнена один раз, в `backups/` одна копия `schema-v1` + +#### Scenario: Открытый каталог данных +- **WHEN** хаб стартует, каталог данных существует с правами `0755` +- **THEN** права каталога и `backups/` — `0700` + +### Requirement: Зеркало задачи +Таблица `issue` SHALL хранить задачу Jira как есть: `key` (PK), `id`, `issue_type`, `summary`, `description` (wiki), +`status`, `priority`, `assignee`, `reporter`, `duedate`, `planned_start`, `planned_end`, `epic_key`, `labels` (JSON-список), +`sprint`, `jira_updated`, `fetched_at`, `neighbor` (0/1 — вне базовой JQL, дочитана по связи), `created`. Таблица `issue_link` +SHALL хранить связи `(source_key, target_key, link_type)` с направлением по семантике Jira: `source` блокирует/ссылается +на `target` (у `source` в GET это `outwardIssue`). Таблица `issue_comment` SHALL хранить комментарии задачи как есть: +`key`, `comment_id` (PK вместе с `key`), `author`, `created`, `updated`, `body` (wiki). Обновлять `issue`, `issue_link` и +`issue_comment` SHALL только загрузка состояния от агента; HTTP API интерфейсов и MCP MUST NOT писать в них. Присланные +комментарии задачи SHALL заменять все её прежние комментарии; задача, присланная без поля `comments`, свои комментарии +SHALL сохранять. Полный прогон (`refresh` по базовой JQL), завершившийся `done`, SHALL помечать `neighbor = 1` задачи, не +обновлённые им, — только если прогон загрузил хотя бы одну задачу. + +#### Scenario: Загрузка состояния заменяет связи +- **WHEN** агент прислал состояние задачи X с двумя связями +- **THEN** все строки `issue_link` с `source_key = X` заменены на присланные; строки с `target_key = X`, не присланные + со стороны X, не трогаются до загрузки их источника + +#### Scenario: Соседняя задача +- **WHEN** в `issuelinks` задачи из базовой JQL есть ключ Y вне JQL +- **THEN** Y сохраняется с `neighbor = 1`; при следующем полном `refresh`, если Y вошла в JQL, `neighbor = 0` + +#### Scenario: Пустой полный прогон +- **WHEN** полный `refresh` завершился `done`, загрузив 0 задач +- **THEN** признак `neighbor` задач зеркала не меняется + +#### Scenario: Комментарий удалён в Jira +- **WHEN** у X в зеркале комментарии 101 и 102, агент прислал X с `comments = [{id: "102", …}]` +- **THEN** у X в `issue_comment` только 102 + +#### Scenario: Состояние от агента до v10 +- **WHEN** у X в зеркале три комментария, агент v9 прислал X без поля `comments` +- **THEN** поля X обновлены, три комментария на месте diff --git a/openspec/changes/issue-card-modal/specs/hub-api/spec.md b/openspec/changes/issue-card-modal/specs/hub-api/spec.md new file mode 100644 index 0000000..56601aa --- /dev/null +++ b/openspec/changes/issue-card-modal/specs/hub-api/spec.md @@ -0,0 +1,125 @@ +## MODIFIED Requirements + +### Requirement: Маршруты агента +Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном): +- `POST /agent/claim` `{agent_version, settings_version, last_jira_call_at, update_state, launcher_version}` — долгий + запрос до 30 с; ответ `{command: {id, type, params, pause_seconds}, settings_version, agent_release}` или `{command: + null, settings_version, agent_release}`, где `agent_release` — `{version, sha256}` опубликованной версии агента или + `null`; обновляет `agent_status` (`last_jira_call_at = null` не затирает прежнее значение). `update_state` (объект или + `null`, `error` хранится до 500 символов) и `launcher_version` (целое или `null`) необязательны; хаб SHALL хранить + последние значения в памяти процесса, значения неверного типа игнорировать, не отвергая `claim`. Если есть команда в + `claimed`, `claim` SHALL выдать её снова (повторная выдача, см. `commands`), а не следующую из очереди. +- `GET /agent/settings` — настройки для агента (без токенов). +- `GET /agent/commands/{id}` — команда по id (для восстановления из журнала), включая `pause_seconds`. +- `POST /agent/commands/{id}/result` `{status, result, error, jira_calls, rate_limited}` — идемпотентно: повторный + отчёт по команде в терминальном статусе отвечает 200 без изменений, кроме позднего отчёта `done` с `result.key` по + `ensure_issue` в `cancelled` или `failed` без результата (результат записывается, статус не меняется, заметка `new:N` + переносится на ключ). Отчёт по команде в `queued` SHALL отвергаться 409 `not_claimed`. `error` хаб SHALL обрезать до + 1500 символов. +- `POST /agent/state` `{command_id, issues: [...], links: [...], neighbor_keys: [...], page, last_page}` — загрузка состояния + пачками; `neighbor_keys` — ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало и `sync_run`. Каждая + пачка применяется независимо; `page` и `last_page` необязательны и хабом не используются. Поле задачи `comments` + необязательно; если оно есть, хаб SHALL до записи проверить его форму: список объектов, `id` — непустая строка, `body` — + строка, `author`, `created`, `updated` — строка или `null`; нарушение — 422 `bad_state` с ключом задачи и именем поля, + пачка не применяется. +- `GET /agent/known-keys` — ключи зеркала (для определения соседей). +- `GET /agent/release` — опубликованная версия агента `{version, sha256, content}` (`content` — текст `agent.py`); + нет публикации или файл не сходится с манифестом по sha256 — 404 `no_release`. + +#### Scenario: Выдача с подстановкой ключа +- **WHEN** следующая команда ссылается на `new:60`, а `ensure_issue` для `new:60` завершилась с `AURORA-1590` +- **THEN** `claim` отдаёт команду с `AURORA-1590` в параметрах + +#### Scenario: Повторный отчёт +- **WHEN** агент дважды прислал `result` для команды 15 +- **THEN** второй ответ 200, статус и результат не изменились + +#### Scenario: Отчёт по невыданной команде +- **WHEN** `POST /agent/commands/16/result` с `status = done`, команда 16 в `queued` +- **THEN** 409 `not_claimed`, команда осталась `queued` + +#### Scenario: Длинная ошибка +- **WHEN** агент прислал `failed` с `error` длиной 5000 символов +- **THEN** в `command.error` первые 1500 символов + +#### Scenario: Поздний отчёт после предела выдач +- **WHEN** `ensure_issue {temp_key: new:8, …}` переведена хабом в `failed` «агент не отчитался после 3 выдач», затем агент + прислал `done` с `result.key = AURORA-1630` +- **THEN** команда `failed`, в `result` — `AURORA-1630`; заметка `new:8` перенесена на `AURORA-1630` + +#### Scenario: Публикация в ответе claim +- **WHEN** опубликована v6, агент v5 отправил `claim`, команд нет +- **THEN** ответ `{command: null, settings_version, agent_release: {version: 6, sha256: …}}` + +#### Scenario: Состояние обновления от агента +- **WHEN** агент прислал в `claim` `update_state = {state: rolled_back, from_version: 6, to_version: 7, error: "код 1 через 3 с"}` + и `launcher_version = 1` +- **THEN** `GET /agent` отдаёт это `update_state`, `launcher_version = 1` и `update_hint` «откат с v7 на v6: код 1 через 3 с» + +#### Scenario: Скачивание версии +- **WHEN** агентским токеном `GET /agent/release`, опубликована v6 +- **THEN** 200 `{version: 6, sha256, content}`, sha256 от `content` в UTF-8 равен `sha256`; клиентским токеном — 401; + без публикации — 404 `no_release` + +#### Scenario: Неверный комментарий в пачке +- **WHEN** `POST /agent/state` с задачами X и Y, у Y комментарий `{id: "7", body: 42}` +- **THEN** 422 `bad_state` с ключом Y и полем `body`; ни X, ни Y в зеркале не изменились + +### Requirement: Маршруты интерфейсов +Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном: +- `GET /issues` с фильтрами `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, + `q` (подстрока темы), `limit`/`offset` — строки зеркала (все поля `issue`) с `fact_status` и `work_kind` из заметок; + сокращение до карточки — дело интерфейса. +- `GET /issues/{key}` — задача целиком: поля, `epic_summary` (тема эпика из зеркала или `null`), связи в обе стороны + (`links_out: [{target_key, link_type, summary, status}]`, `links_in: [{source_key, link_type, summary, status}]`, где + `summary` и `status` — связанной задачи из зеркала или `null`, если её там нет), заметки, последние 10 команд по ней, + `comments: [{comment_id, author, created, updated, body}]` — все комментарии, новые сверху (по `created`, затем + `comment_id`, по убыванию). +- `PUT /issues/{key}/note` — заметки (частичное обновление, `updated_by` из тела). +- `POST /batches` `{title, author, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе, неверных + параметрах (в том числе неверного типа значения), временном ключе без `ensure_issue`, двух и более `ensure_issue` с + одинаковой `summary` (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы). +- `GET /batches?limit=`, `GET /batches/{id}` (с командами), `POST /batches/{id}/cancel` — отменяет `queued` команды + пакета; ответ — вид пакета с полем `still_claimed`: список `{id, type}` команд пакета в `claimed` (они будут выполнены + агентом; остановить — отмена команды с `force`). +- `POST /commands/{id}/retry` — новая команда с теми же параметрами в новом пакете «Повтор команды N». +- `POST /commands/{id}/cancel` — только для `queued`; для `claimed` — 409, если не передан `force: true`. +- `POST /refresh` `{keys | jql | full: true}` → пакет из одного `refresh`. +- `GET /agent`, `GET /sync-runs?limit=`, `GET /settings`, `PUT /settings` (частично; поднимает `settings_version`; + ответ — все настройки после записи, как у `GET /settings`, включая `settings_version`). + +#### Scenario: Фильтр по спринту +- **WHEN** `GET /issues?sprint=Спринт 2` +- **THEN** только задачи с таким `sprint`, отсортированы по сроку, затем по ключу + +#### Scenario: Отмена взятой команды +- **WHEN** `POST /commands/15/cancel` без `force`, команда `claimed` +- **THEN** 409 с `claimed_at`; с `force: true` — `cancelled`, следующий отчёт агента по ней принимается, статус не меняется + +#### Scenario: Поздний отчёт по отменённому созданию +- **WHEN** `ensure_issue {temp_key: new:7, …}` отменена с `force: true`, затем агент прислал `done` с `result.key = AURORA-1620` +- **THEN** команда `cancelled`, в `result` — `AURORA-1620`; заметка `new:7` перенесена на `AURORA-1620` + +#### Scenario: Дубль темы в пакете +- **WHEN** `POST /batches` с двумя `ensure_issue`, у обеих `summary = "Шахматка: комментарии"` +- **THEN** 422 с темой «Шахматка: комментарии», пакет не создан + +#### Scenario: Тема не строка +- **WHEN** `POST /batches` с `ensure_issue {temp_key: new:1, summary: 42, …}` +- **THEN** 422 с указанием параметра `summary`, пакет не создан + +#### Scenario: Отмена пакета с командой у агента +- **WHEN** в пакете 9 команда 30 `claimed`, команды 31 и 32 `queued`; `POST /batches/9/cancel` +- **THEN** 31 и 32 `cancelled`; в ответе `still_claimed = [{id: 30, type: …}]`; 30 остаётся `claimed` + +#### Scenario: Ответ на запись настроек +- **WHEN** `PUT /settings {relay_dir: "/Volumes/externalSSD/utm/rshb/jira-bridge"}` +- **THEN** 200; ответ содержит `relay_dir` с записанным значением, новую `settings_version` и остальные настройки; + токенов в ответе нет + +#### Scenario: Карточка с комментариями и связями +- **WHEN** у AURORA-9001 два комментария (14.10 и 15.10), исходящая связь на AURORA-9002 («API свода», «Новая») и на + AURORA-1, которой нет в зеркале; эпик AURORA-9100 «Тестовый эпик» +- **THEN** `GET /issues/AURORA-9001` отдаёт `comments` с комментарием от 15.10 первым, у связи на AURORA-9002 — + `summary = "API свода"`, `status = "Новая"`, у связи на AURORA-1 — `summary = null`, `status = null`; + `epic_summary = "Тестовый эпик"` diff --git a/openspec/changes/issue-card-modal/specs/mcp/spec.md b/openspec/changes/issue-card-modal/specs/mcp/spec.md new file mode 100644 index 0000000..3c3186e --- /dev/null +++ b/openspec/changes/issue-card-modal/specs/mcp/spec.md @@ -0,0 +1,53 @@ +## MODIFIED Requirements + +### Requirement: Инструменты +| Инструмент | Параметры | Результат | +|---|---|---| +| `issues` | фильтры как у `GET /issues`, `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту | +| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет» | +| `notes_set` | `key`, подмножество полей заметок | обновлённые заметки | +| `batch_create` | `title`, `commands` (список объектов по контракту `commands`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации | +| `batch_status` | `id` | статусы команд, ошибки, оценка остатка | +| `batch_wait` | `id`, `timeout_minutes` (≤ 60) | как `batch_status`, возвращается по завершении или по таймауту | +| `batches` | `limit` | последние пакеты со статусом | +| `command_retry` | `id` | новый пакет | +| `batch_cancel` | `id` | вид пакета; для команд пакета, уже взятых агентом, — строка «команда #N уже у агента и будет выполнена; остановить — `POST /commands/N/cancel` с `force: true`» | +| `refresh` | `keys` или `full`, `wait` (bool) | без `wait` — строка с id поставленного пакета; с `wait` — итог пакета, как у `batch_wait` | +| `agent_status` | — | онлайн/офлайн, версия, текущая команда, довыданные команды (id, тип, число выдач), 429 за сутки, версия настроек; публикация агента («публикация: v от <дата>» или «публикации нет»), состояние обновления («обновление: v → v»), запускатель («запускатель: v», «без запускателя» или, пока `agent_reported = false`, «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска хаба)»), `update_hint`, если он есть | +| `sync_log` | `limit` | последние `sync_run` | +| `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` | + +Описание инструмента `batch_create` SHALL предупреждать, что `ensure_issue` должна нести `labels` с базовой меткой +проекта (`AWPCA`): хаб и агент метки сами не добавляют, и задача без неё выпадает из базовой JQL и из зеркала. + +Автор команд из MCP SHALL быть `claude`; из API без автора — `api`. + +#### Scenario: Пакет из чата +- **WHEN** `batch_create {title: "1538 в спринт 2", commands: [ensure_fields{…}, ensure_link{…}, ensure_sprint{…}]}` +- **THEN** пакет создан с четырьмя командами (три + `refresh`), ответ содержит оценку в минутах + +#### Scenario: Ожидание результата +- **WHEN** `batch_wait {id, timeout_minutes: 10}` и пакет завершился через 6 минут +- **THEN** ответ через 6 минут со статусами всех команд + +#### Scenario: Статус агента с довыдачей +- **WHEN** агент на связи, команда 40 выдана дважды и ещё без отчёта +- **THEN** ответ `agent_status` содержит «агент онлайн» и строку о команде 40 с числом выдач 2 + +#### Scenario: Отмена пакета с командой у агента +- **WHEN** `batch_cancel {id: 9}`, команда 30 пакета в `claimed` +- **THEN** ответ содержит вид пакета и строку «команда #30 уже у агента и будет выполнена; остановить — …» + +#### Scenario: Статус агента после отката +- **WHEN** опубликована v7, агент v6 под запускателем v1 прислал `update_state = rolled_back` для v7 +- **THEN** ответ `agent_status` содержит «публикация: v7 от …», «обновление: rolled_back v6 → v7», «запускатель: v1» и + «откат с v7 на v6: …» + +#### Scenario: Статус агента сразу после перезапуска хаба +- **WHEN** хаб перезапущен, агент ещё не прислал `claim` +- **THEN** ответ `agent_status` содержит «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска + хаба)» и не содержит «без запускателя» и «агент запущен без run.py» + +#### Scenario: Задача с семью комментариями +- **WHEN** `issue {key: "AURORA-9001"}`, у задачи 7 комментариев, у самого нового текст 500 символов +- **THEN** ответ содержит «комментарии (7):», 5 последних комментариев (самый новый — первым, обрезан до 300 символов с «…») и «ещё 2» diff --git a/openspec/changes/issue-card-modal/specs/web-app/spec.md b/openspec/changes/issue-card-modal/specs/web-app/spec.md new file mode 100644 index 0000000..d669499 --- /dev/null +++ b/openspec/changes/issue-card-modal/specs/web-app/spec.md @@ -0,0 +1,162 @@ +## MODIFIED Requirements + +### Requirement: Оболочка и адреса +Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список +задач) и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь — +на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать переходы «Доска», «Задачи» и действие +«Сменить токен». Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не +пустым экраном. + +#### Scenario: Переход между разделами +- **WHEN** на доске выбран раздел «Задачи» +- **THEN** адрес — `/app/issues`, показан список задач; «назад» возвращает на доску + +### 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. Пока окно открыто +и после его закрытия доска SHALL сохранять прокрутку, раскладку, фильтры и строку состояния пакета. Действия «Сменить токен» в меню +доски нет — оно в верхней панели приложения. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку +карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке +захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет. +При возврате на `/app/board` с другой страницы приложения в той же вкладке доска SHALL восстанавливать прежнюю +прокрутку; после перезагрузки страницы доска открывается сверху. + +#### Scenario: Раскладка переживает переход +- **WHEN** раскладка карточек сохранена страницей `links.html`, открыта `/app/board` с теми же данными +- **THEN** карточки стоят на тех же местах, дорожки в том же порядке и свёртке, фильтры те же + +#### Scenario: Совпадение со старой доской +- **WHEN** на одной фикстуре `GET /board` и одном наборе фильтров построены модель, видимая выборка, критический путь + и проблемы старой страницей и приложением +- **THEN** результаты совпадают + +#### Scenario: Открыть карточку +- **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку» +- **THEN** адрес `/app/issues/AURORA-1620`, карточка задачи открыта окном, под ним доска + +#### Scenario: Возврат с карточки +- **WHEN** доска прокручена вниз, нажат значок «Открыть карточку», затем окно закрыто +- **THEN** адрес `/app/board`, доска показана с той же прокруткой и раскладкой, без повторной загрузки `GET /board` + +#### Scenario: Возврат из другого раздела +- **WHEN** доска прокручена вниз, выбран раздел «Задачи», затем раздел «Доска» +- **THEN** доска показана с той же прокруткой + +### Requirement: Список задач +Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (открывает карточку задачи окном поверх списка), тема, +тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы, +статус по факту, признак «сосед». Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид +работы, статус по факту, статус Jira, метка, эпик, только соседи, поиск по ключу или теме), храниться в query-строке +адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`; значение фильтра из адреса, которого нет среди +вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Задачи SHALL +загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя порция полная. + +#### Scenario: Фильтр в адресе +- **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель +- **THEN** адрес содержит `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого + адреса в новой вкладке показывает ту же выборку + +#### Scenario: Конец списка +- **WHEN** вторая порция вернула 17 задач +- **THEN** в таблице 67 задач, «Показать ещё» не показана + +#### Scenario: Фильтр по закрытому спринту +- **WHEN** открыт адрес `/app/issues?sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет +- **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру + +#### Scenario: Карточка поверх списка +- **WHEN** список отфильтрован по спринту, прокручен и догружен «Показать ещё», открыта карточка задачи и окно закрыто +- **THEN** адрес списка с тем же фильтром, в таблице те же строки, прокрутка та же + +### Requirement: Карточка задачи +Адрес `/app/issues/{key}` SHALL открывать карточку задачи из `GET /issues/{key}` модальным окном поверх страницы, с +которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры, +загруженные строки, прокрутку и раскладку. Открытый напрямую адрес (новая вкладка, закладка, перезагрузка) SHALL +показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону и +возвращать на страницу, с которой его открыли, даже после переходов по связям внутри окна; «назад» браузера после +закрытия MUST NOT открывать карточку снова. Переход по связанной задаче SHALL менять содержимое окна и адрес, «назад» +браузера — возвращать к прежней карточке. Пока окно открыто, фокус SHALL оставаться в окне, страница под ним не +прокручивается. + +Содержимое окна: +- шапка: ключ, статус Jira, статус по факту (цветом класса из `status_labels`), «Открыть в Jira», ✕, тема; строка + «исполнитель · срок · спринт · эпик» без пустых частей, срок — `planned_end`, иначе `duedate`, с пометкой «просрочено», + если он раньше сегодняшней даты и класс статуса по факту не `fact-done` и не `fact-cancelled`; эпик — темой + (`epic_summary`), иначе ключом; +- «Связи» по группам в порядке: «Зависит от» (входящие связи логических типов `блокирует начало` и `блокирует + завершение` из `link_types`, с меткой «начало» / «завершение»), «Блокирует» (исходящие тех же типов, с теми же метками), + «Связана с» (тип `связанные` в обе стороны), «Прочие» (тип Jira вне `link_types`: имя типа и направление «→» / «←»); + в строке — ключ (открывает связанную задачу в окне), тема и статус Jira, а если задачи нет в зеркале — ключ и «нет в + зеркале»; пустые группы не показываются, без связей — «связей нет»; +- «Описание» — текст с сохранёнными переносами строк, без разбора разметки Jira; длиннее 8 строк — свёрнуто с кнопкой + «развернуть» / «свернуть»; без описания раздела нет; +- «Комментарии (N)» — новые сверху, автор, время, текст с сохранёнными переносами без разбора разметки; кнопка + «Скрыть» / «Показать» сворачивает раздел, выбор SHALL запоминаться в браузере для всех карточек; без комментариев — + «комментариев нет»; +- «Служебные заметки» (требование «Служебные заметки»); +- «Прочие поля» — непустые из: тип, приоритет, автор, метки, плановое начало, обновлена в Jira, сосед по связям; +- «Команды моста (N)» — 10 последних команд по задаче (id, пакет, тип, статус, время завершения, ошибка), раздел свёрнут + при открытии; если команд не было — текст «команд по задаче не было». +На ширине от 900 px «Связи», «Описание», «Комментарии» — в левой колонке, заметки, прочие поля и команды — в правой; +уже — одна колонка: заметки, связи, описание, комментарии, прочие поля, команды. Если задачи нет в зеркале (404), окно +SHALL сообщать об этом с кнопкой «Закрыть»; хаб не отвечает — сообщение и «Повторить», ранее загруженные данные +остаются. + +#### Scenario: Связанная задача +- **WHEN** у AURORA-1620 исходящая связь `ASUPR_Блокер_Начала` на AURORA-1621 «Расчёт свода» в статусе «В работе» +- **THEN** в карточке в группе «Блокирует» строка «начало · AURORA-1621 · Расчёт свода · В работе»; щелчок по ключу + меняет содержимое окна на AURORA-1621, адрес — `/app/issues/AURORA-1621`; «назад» браузера возвращает AURORA-1620 + +#### Scenario: Входящий блокер +- **WHEN** у AURORA-1620 входящая связь `ASUPR_Блокер_Завершения` от AURORA-1500 +- **THEN** строка с AURORA-1500 и меткой «завершение» — в группе «Зависит от» + +#### Scenario: Задача без команд +- **WHEN** по AURORA-1620 не было команд +- **THEN** в разделе «Команды моста» — текст «команд по задаче не было», таблицы нет + +#### Scenario: Закрытие после переходов +- **WHEN** со списка «Задачи» открыта AURORA-1620, в окне — переход по связи на AURORA-1621, нажата Esc +- **THEN** окно закрыто, адрес — адрес списка с прежними фильтрами; «назад» браузера не открывает карточку + +#### Scenario: Прямой адрес +- **WHEN** в новой вкладке открыт `/app/issues/AURORA-1620`, затем нажат ✕ +- **THEN** под окном был список задач без фильтров; после закрытия адрес `/app/issues`, «назад» не открывает карточку + +#### Scenario: Скрытые комментарии +- **WHEN** в карточке нажато «Скрыть» у комментариев, затем страница перезагружена и открыта другая карточка +- **THEN** комментарии в ней скрыты, видна кнопка «Показать» и число комментариев + +### Requirement: Служебные заметки +Карточка задачи SHALL показывать служебные заметки в режиме чтения: «Вид работы», «Статус по факту», «Сделано», +«Осталось», «Заметка владельца» (пустые — «—») и время и автор последнего изменения; кнопка «Править» SHALL открывать +форму: «вид работы» — выбор из значений `kind_labels` настроек (и текущего значения, если его там нет, и пустого), +«статус по факту» — текст с подсказками из ключей `status_labels`, «сделано», «осталось», «заметка владельца» — +многострочный текст; «Отмена» SHALL возвращать режим чтения, при несохранённых правках — после подтверждения. +Сохранение SHALL отправлять `PUT /issues/{key}/note` только с изменёнными полями и `updated_by: "web"`; очищенное поле +отправляется как `null`. После успеха SHALL показываться режим чтения с сохранённой заметкой; страница под окном (список, +доска) SHALL показывать новые «вид работы» и «статус по факту» без «Перечитать»; при 422 — текст ошибки у формы без +потери введённого. Закрытие окна, переход по связи и уход в другой раздел с несохранёнными правками SHALL требовать +подтверждения. Заметки пишутся только в хаб, в Jira приложение ничего не отправляет. + +#### Scenario: Только изменённые поля +- **WHEN** в заметке нажато «Править», изменено только «осталось» и нажато «Сохранить» +- **THEN** тело `PUT /issues/{key}/note` — `{rest_text: "…", updated_by: "web"}`; карточка снова в режиме чтения с новым + «Осталось» + +#### Scenario: Уход с несохранёнными правками +- **WHEN** в форме заметки есть несохранённая правка и нажата Esc +- **THEN** приложение спрашивает подтверждение; при отказе окно открыто, форма с введённым текстом + +#### Scenario: Заметка видна на доске +- **WHEN** карточка AURORA-9002 открыта с доски, вид работы изменён на «тестирование» и сохранён, окно закрыто +- **THEN** карточка AURORA-9002 на доске показывает вид работы «тестирование»