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

169 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 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` (пишет в
каталог живого хаба: выполняет владелец или агент по его отдельной команде).
### Хаб
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, direct}`: `background` — адрес страницы под окном (`pathname + search`),
`depth` — сколько записей истории занято карточками поверх неё (1 при открытии со страницы, +1 при переходе по
связи), `direct` — окно открыто прямым адресом (переносится в переходы по связям). Маршрут верхнего уровня —
`{path: "*", element: <Layout/>}`; `Layout` всегда рисует страницы через `useRoutes(PAGE_ROUTES, pageLocation)` с
явным адресом: `background` при открытом окне, иначе текущий адрес. Явный адрес нужен всегда: так дерево
компонентов страницы не меняется при открытии окна (страница не пересоздаётся), а `useSearchParams`/`useLocation`
внутри фоновой страницы видят её собственный адрес, и фильтры списка не сбрасываются. Состояние страницы (фильтры,
загруженные порции, прокрутка, раскладка доски, опрос пакета) сохраняется. `ErrorBoundary` страницы получает ключ по
адресу страницы, у окна — свой.
11. **Прямой адрес** `/app/issues/{key}` без `state` (новая вкладка, закладка): `Layout` заменяет запись истории тем же
адресом со `state = {background: /issues, depth: 0, direct: true}`; под окном — список задач без фильтров.
Перезагрузка страницы с открытым окном сохраняет `state` (браузер хранит его в записи истории).
12. **Закрытие** (✕, Esc, щелчок по фону): `depth = 0` — `navigate("/issues", {replace: true})`; `depth ≥ 1` —
`navigate(-depth)`, браузер возвращается к записи страницы под окном, и «назад» после закрытия ведёт на шаг раньше
неё; если окно было открыто прямым адресом (`direct`), запись прямого адреса, на которую вернулись, заменяется на
`/issues` (`replace`). Закрытие проходит через блокировщик заметок (п. 14). Вкладки «Доска»/«Задачи» в шапке
подсвечиваются по адресу страницы под окном.
13. **Элемент `<dialog>`** с `showModal()`: фокус удерживается в окне, фон недоступен, Esc вызывает `cancel` (обрабатываем
сами — закрытие по п. 12 с проверкой заметок; если `cancel` не отменяемый — браузер закрыл окно сам без активации
пользователя, — по событию `close` окно открывается снова и закрытие идёт тем же путём). Щелчок по фону — щелчок по
самому `<dialog>` вне области содержимого.
Прокрутка страницы под окном на время открытия блокируется (`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)** — свёрнуты (`<details>`), внутри нынешняя таблица; команд нет — «команд по задаче не было» и
раздел не сворачивается.
23. **Колонки**: под шапкой слева — «Связи», «Описание», «Комментарии»; справа (≈320 px) «Заметки», «Прочие
поля», «Команды моста». Уже 900 px — одна колонка: заметки, связи, описание, комментарии, прочие поля, команды.
24. **Состояния окна**: загрузка — «загрузка <key>…»; 404 — «Задачи <key> нет в зеркале хаба.» и «Закрыть»; хаб не
отвечает — сообщение и «Повторить», ранее загруженные данные остаются; поверх ошибки при данных — предупреждение
в шапке окна.
### Проверки
25. **Тестовый хаб**: `web/scripts/seed-extras.json` — дополнения к фикстуре доски только для тестового хаба: у AURORA-9001
описание длиннее 8 строк, три комментария и две команды (пакет автора `seed`, команды завершены). `seed-hub.py`
применяет их после фикстуры. Связи не добавляются (они меняют доску и её e2e); группа «Прочие» и связь на задачу вне
зеркала проверяются в vitest, а связь на задачу вне зеркала (`lower-1`) уже есть в фикстуре. Фикстура доски и её
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 дизайна был только «Править»/«Сохранить») | Из режима правки нужен выход без сохранения, не закрывая окно | Лишний вопрос при отмене |