Andrey Epifancev 9484d10d0b docs: план issue-card-modal (#37); уточнения design 5, 10–13, 25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:52:09 +04:00

22 KiB
Raw Blame History

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 с <Outlet>; маршруты 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 (пишет в каталог живого хаба: выполняет владелец или агент по его отдельной команде).

Хаб

  1. Таблица issue_comment (key, comment_id, author, created, updated, body, PRIMARY KEY (key, comment_id)); выборка по key идёт по первичному ключу, отдельный индекс не нужен. Схема v3; миграция v2 → v3 создаёт таблицу (копия базы schema-v2 — существующим механизмом).
  2. Запись. apply_state: для задачи с полем comments (список) комментарии этой задачи удаляются и вставляются присланные — в одной транзакции с задачей; задача без поля comments своих комментариев не теряет (агент до v10).
  3. Проверка формы в h_agent_state до записи: comments — список объектов; id — непустая строка; body — строка; author, created, updated — строка или null. Нарушение — 422 bad_state с ключом задачи и полем, пачка не применяется целиком (агент получает ошибку хаба в отчёте refresh).
  4. GET /issues/{key}: добавляются comments (новые сверху: по created убыв., затем comment_id убыв.), epic_summary (тема задачи-эпика из зеркала или null), у каждой связи — summary и status связанной задачи (LEFT JOIN issue, null, если её нет в зеркале). Прежние поля и их форма не меняются.
  5. MCP issue: после заметок — «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), затем «ещё M», если их больше 5; нет комментариев — «комментариев нет».

Веб-приложение: окно

  1. Фоновая страница. Ссылки на карточку (ключ в списке, «ⓘ» на доске, связи в карточке) передают в location.state {background, depth, direct}: background — адрес страницы под окном (pathname + search), depth — сколько записей истории занято карточками поверх неё (1 при открытии со страницы, +1 при переходе по связи), direct — окно открыто прямым адресом (переносится в переходы по связям). Маршрут верхнего уровня — {path: "*", element: <Layout/>}; Layout всегда рисует страницы через useRoutes(PAGE_ROUTES, pageLocation) с явным адресом: background при открытом окне, иначе текущий адрес. Явный адрес нужен всегда: так дерево компонентов страницы не меняется при открытии окна (страница не пересоздаётся), а useSearchParams/useLocation внутри фоновой страницы видят её собственный адрес, и фильтры списка не сбрасываются. Состояние страницы (фильтры, загруженные порции, прокрутка, раскладка доски, опрос пакета) сохраняется. ErrorBoundary страницы получает ключ по адресу страницы, у окна — свой.
  2. Прямой адрес /app/issues/{key} без state (новая вкладка, закладка): Layout заменяет запись истории тем же адресом со state = {background: /issues, depth: 0, direct: true}; под окном — список задач без фильтров. Перезагрузка страницы с открытым окном сохраняет state (браузер хранит его в записи истории).
  3. Закрытие (✕, Esc, щелчок по фону): depth = 0 — navigate("/issues", {replace: true}); depth ≥ 1 — navigate(-depth), браузер возвращается к записи страницы под окном, и «назад» после закрытия ведёт на шаг раньше неё; если окно было открыто прямым адресом (direct), запись прямого адреса, на которую вернулись, заменяется на /issues (replace). Закрытие проходит через блокировщик заметок (п. 14). Вкладки «Доска»/«Задачи» в шапке подсвечиваются по адресу страницы под окном.
  4. Элемент <dialog> с showModal(): фокус удерживается в окне, фон недоступен, Esc вызывает cancel (обрабатываем сами — закрытие по п. 12 с проверкой заметок; если cancel не отменяемый — браузер закрыл окно сам без активации пользователя, — по событию close окно открывается снова и закрытие идёт тем же путём). Щелчок по фону — щелчок по самому <dialog> вне области содержимого. Прокрутка страницы под окном на время открытия блокируется (overflow: hidden у body), содержимое окна прокручивается внутри, шапка окна закреплена. Ширина до 1000 px, высота до 90vh; на экранах уже 700 px — на всю ширину.
  5. Несохранённые заметки. Подтверждение «Есть несохранённые правки заметки. Уйти без сохранения?» при закрытии окна, переходе по связи и уходе в раздел; условие блокировки — смена pathname при правках (как сейчас), закрытие по п. 12 проходит через тот же блокировщик. Отказ — окно остаётся с введённым текстом.
  6. Доска под окном. Сохранение заметки уже инвалидирует board и ["issues"] (useSaveNote): фоновая страница смонтирована, поэтому перечитывается и показывает новые «вид работы» и «статус по факту». Отдельного кода нет — поведение закрепляется тестом.

Веб-приложение: раскладка карточки

  1. Шапка: ключ; статус Jira (плашка); статус по факту (плашка цветом класса из status_labels, если есть); «Открыть в Jira ↗»; ✕; тема; строка «исполнитель · срок · спринт · эпик: <тема эпика>» — пустые части пропускаются, эпик без темы в зеркале — ключом. Срок — planned_end, иначе duedate; «просрочено» (красным) — срок раньше сегодняшней даты и статус по факту не относится к классам fact-done, fact-cancelled.
  2. Связи по смыслу. По link_types настроек: входящие блокирует начало / блокирует завершение — группа «Зависит от», исходящие — «Блокирует»; у строки метка «начало» / «завершение». связанные в обе стороны — «Связана с». Тип, которого нет в link_types, — «Прочие» с именем типа Jira и направлением («→» исходящая, «←» входящая). Строка: ключ (ссылка — открывает связанную задачу в окне), тема, статус Jira (плашка); связанной задачи нет в зеркале — ключ и «нет в зеркале». Пустая группа не показывается; связей нет — «связей нет». Порядок групп: «Зависит от», «Блокирует», «Связана с», «Прочие»; внутри — по ключу.
  3. Описание — pre-wrap без разбора разметки; выше 8 строк сворачивается с кнопкой «развернуть» / «свернуть» (кнопка только если текст не помещается). Нет описания — раздела нет.
  4. Комментарии — «Комментарии (N)» с кнопкой «Скрыть» / «Показать»; новые сверху; автор, время (fmtStamp), текст pre-wrap. Выбор «скрыто» — один на все карточки, в localStorage jira-hub-comments-hidden ("1" — скрыты); недоступный localStorage — показаны. Нет комментариев — «комментариев нет» (кнопки нет).
  5. Заметки — по умолчанию режим чтения: «Вид работы», «Статус по факту», «Сделано», «Осталось», «Заметка владельца» (пустые — «—»), строка «изменено <время>, <автор>»; кнопка «Править» показывает нынешнюю форму NoteForm (поля, «Сохранить», «Отмена»); после успешного сохранения — снова режим чтения; «Отмена» без правок — сразу, с правками — подтверждение «Отменить правки заметки?». Заметки ещё нет — режим чтения с «—» и «Править».
  6. Прочие поля: тип, приоритет, автор, метки, плановое начало, обновлена в Jira, «сосед по связям» — непустые.
  7. Команды моста (N) — свёрнуты (<details>), внутри нынешняя таблица; команд нет — «команд по задаче не было» и раздел не сворачивается.
  8. Колонки: под шапкой слева — «Связи», «Описание», «Комментарии»; справа (≈320 px) «Заметки», «Прочие поля», «Команды моста». Уже 900 px — одна колонка: заметки, связи, описание, комментарии, прочие поля, команды.
  9. Состояния окна: загрузка — «загрузка …»; 404 — «Задачи нет в зеркале хаба.» и «Закрыть»; хаб не отвечает — сообщение и «Повторить», ранее загруженные данные остаются; поверх ошибки при данных — предупреждение в шапке окна.

Проверки

  1. Тестовый хаб: web/scripts/seed-extras.json — дополнения к фикстуре доски только для тестового хаба: у AURORA-9001 описание длиннее 8 строк, три комментария и две команды (пакет автора seed, команды завершены). seed-hub.py применяет их после фикстуры. Связи не добавляются (они меняют доску и её e2e); группа «Прочие» и связь на задачу вне зеркала проверяются в vitest, а связь на задачу вне зеркала (lower-1) уже есть в фикстуре. Фикстура доски и её golden-эталоны не меняются.
  2. 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 дизайна был только «Править»/«Сохранить») Из режима правки нужен выход без сохранения, не закрывая окно Лишний вопрос при отмене