Merge pull request 'docs(openspec): архив issue-card-modal (#37)' (#41) from chore/37-archive-issue-card-modal into main

This commit is contained in:
aep 2026-10-15 14:32:41 +03:00
commit ce9f5a1894
13 changed files with 159 additions and 38 deletions

View File

@ -108,11 +108,16 @@ Jira в текущем процессе, до него — `null`. На HTTP 429
- **THEN** `last_jira_call_at` в `claim` — `null`; после первого вызова Jira следующий `claim` несёт его время
### Requirement: Чтение состояния
`refresh` SHALL выполняться запросом `search` (`jql`, `fields` = поля зеркала включая `issuelinks` и спринт,
`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 задачах
@ -123,6 +128,20 @@ Jira в текущем процессе, до него — `null`. На HTTP 429
- **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 = []`
### Requirement: Доставка и запуск
README агента SHALL описывать: первую установку (копирование `agent.py`, `run.py` и `agent.example.json` через общую
папку в локальный каталог виртуалки, заполнение `agent.json` с выбором транспорта в `hub_url`, `check`), запуск

View File

@ -6,14 +6,15 @@
## Requirements
### Requirement: Схема базы хаба
База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_note`, `batch`, `command`,
`sync_run`, `agent_status`, `setting`. Схема SHALL создаваться и мигрироваться самим хабом при старте по номеру версии
в `setting.schema_version` (текущая — 2). Миграция существующей базы SHALL выполняться в одной транзакции с блокировкой
записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в
База хаба 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` и плановых дат (даты без времени).
дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени) и времени
комментариев (строки Jira как есть).
#### Scenario: Первый запуск
- **WHEN** хаб стартует с несуществующим файлом базы
@ -21,8 +22,12 @@
#### Scenario: Миграция базы v1
- **WHEN** хаб стартует с базой `schema_version = 1`, в которой есть задачи, заметки и команда в `claimed`
- **THEN** в `backups/` копия базы v1; добавлена `command.deliveries` (у `claimed` — 1, у остальных — 0); данные на месте;
`schema_version = 2`
- **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` одновременно открываются два соединения
@ -37,9 +42,12 @@
`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` и `issue_link` SHALL только загрузка состояния от
агента; HTTP API интерфейсов и MCP MUST NOT писать в них. Полный прогон (`refresh` по базовой JQL), завершившийся
`done`, SHALL помечать `neighbor = 1` задачи, не обновлённые им, — только если прогон загрузил хотя бы одну задачу.
на `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 с двумя связями
@ -54,6 +62,14 @@ SHALL хранить связи `(source_key, target_key, link_type)` с нап
- **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 обновлены, три комментария на месте
### Requirement: Служебные заметки
Таблица `issue_note` SHALL хранить поля, которых нет в Jira: `key` (PK; ключ Jira или временный `new:<n>`),
`fact_status`, `work_kind`, `done_text`, `rest_text`, `owner_note`, `updated_at`, `updated_by`. Обновление зеркала

View File

@ -44,7 +44,10 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
1500 символов.
- `POST /agent/state` `{command_id, issues: [...], links: [...], neighbor_keys: [...], page, last_page}` — загрузка состояния
пачками; `neighbor_keys` — ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало и `sync_run`. Каждая
пачка применяется независимо; `page` и `last_page` необязательны и хабом не используются.
пачка применяется независимо; `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`.
@ -84,12 +87,20 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
- **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}` — задача целиком: поля, связи в обе стороны, заметки, последние 10 команд по ней.
- `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` с
@ -132,6 +143,13 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
- **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 = "Тестовый эпик"`
### Requirement: Резервные копии
Перед выдачей первой команды пакета, содержащего записи в Jira, хаб SHALL скопировать файл базы в `backups/<дата-время>-<batch>.sqlite`
и хранить последние `backup_keep` копий.

View File

@ -18,7 +18,7 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
| Инструмент | Параметры | Результат |
|---|---|---|
| `issues` | фильтры как у `GET /issues`, `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту |
| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды |
| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет» |
| `notes_set` | `key`, подмножество полей заметок | обновлённые заметки |
| `batch_create` | `title`, `commands` (список объектов по контракту `commands`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации |
| `batch_status` | `id` | статусы команд, ошибки, оценка остатка |
@ -61,3 +61,7 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
- **WHEN** хаб перезапущен, агент ещё не прислал `claim`
- **THEN** ответ `agent_status` содержит «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска
хаба)» и не содержит «без запускателя» и «агент запущен без run.py»
#### Scenario: Задача с семью комментариями
- **WHEN** `issue {key: "AURORA-9001"}`, у задачи 7 комментариев, у самого нового текст 500 символов
- **THEN** ответ содержит «комментарии (7):», 5 последних комментариев (самый новый — первым, обрезан до 300 символов с «…») и «ещё 2»

View File

@ -37,7 +37,7 @@ SHALL очищать токен и показывать экран входа.
### Requirement: Оболочка и адреса
Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
задач) и `/app/issues/{key}` (карточка задачи); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
задач) и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать переходы «Доска», «Задачи» и действие
«Сменить токен». Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не
пустым экраном.
@ -75,7 +75,8 @@ SHALL очищать токен и показывать экран входа.
`localStorage` под теми же ключами `graph-<имя>…`, что у `links.html`, с той же миграцией старых ключей.
Результат построения модели, видимой выборки, критического пути и проблем SHALL совпадать с результатом `links.html`
на одних и тех же данных и фильтрах. У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку»,
ведущий на `/app/issues/{key}`; ключ карточки по-прежнему открывает задачу в Jira. Действия «Сменить токен» в меню
открывающий `/app/issues/{key}` окном поверх доски; ключ карточки по-прежнему открывает задачу в Jira. Пока окно открыто
и после его закрытия доска SHALL сохранять прокрутку, раскладку, фильтры и строку состояния пакета. Действия «Сменить токен» в меню
доски нет — оно в верхней панели приложения. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку
карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке
захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет.
@ -93,10 +94,14 @@ SHALL очищать токен и показывать экран входа.
#### Scenario: Открыть карточку
- **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку»
- **THEN** открыта `/app/issues/AURORA-1620` внутри приложения
- **THEN** адрес `/app/issues/AURORA-1620`, карточка задачи открыта окном, под ним доска
#### Scenario: Возврат с карточки
- **WHEN** доска прокручена вниз, нажат значок «Открыть карточку», затем выбран раздел «Доска»
- **WHEN** доска прокручена вниз, нажат значок «Открыть карточку», затем окно закрыто
- **THEN** адрес `/app/board`, доска показана с той же прокруткой и раскладкой, без повторной загрузки `GET /board`
#### Scenario: Возврат из другого раздела
- **WHEN** доска прокручена вниз, выбран раздел «Задачи», затем раздел «Доска»
- **THEN** доска показана с той же прокруткой
### Requirement: Источник данных доски
@ -144,7 +149,7 @@ SHALL проверять агента (`GET /agent`; офлайн — преду
- **THEN** доска его не опрашивает, кнопка «Обновить из Jira» доступна
### Requirement: Список задач
Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (ссылка на карточку), тема,
Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (открывает карточку задачи окном поверх списка), тема,
тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы,
статус по факту, признак «сосед». Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид
работы, статус по факту, статус Jira, метка, эпик, только соседи, поиск по ключу или теме), храниться в query-строке
@ -165,37 +170,96 @@ SHALL проверять агента (`GET /agent`; офлайн — преду
- **WHEN** открыт адрес `/app/issues?sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет
- **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру
#### Scenario: Карточка поверх списка
- **WHEN** список отфильтрован по спринту, прокручен и догружен «Показать ещё», открыта карточка задачи и окно закрыто
- **THEN** адрес списка с тем же фильтром, в таблице те же строки, прокрутка та же
### Requirement: Карточка задачи
Страница `/app/issues/{key}` SHALL показывать задачу из `GET /issues/{key}`: поля зеркала, ссылку на задачу в Jira,
описание как текст с сохранёнными переносами строк (без разбора разметки Jira), исходящие и входящие связи с подписью
типа (логический тип из `link_types`, иначе имя типа Jira) и ссылками на карточки связанных задач, 10 последних команд
по задаче (id, пакет, тип, статус, время завершения, ошибка), а если команд не было — текст «команд по задаче не было». Если задачи нет в зеркале (404), страница SHALL сообщать
об этом со ссылкой на список.
Адрес `/app/issues/{key}` SHALL открывать карточку задачи из `GET /issues/{key}` модальным окном поверх страницы, с
которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры,
загруженные строки, прокрутку и раскладку. Перезагрузка страницы с открытым окном 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», ключ ведёт на `/app/issues/AURORA-1621`
- **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** в разделе «Последние команды» — текст «команд по задаче не было», таблицы нет
- **THEN** в разделе «Команды моста» — текст «команд по задаче не было», таблицы нет
#### Scenario: Закрытие после переходов
- **WHEN** со списка «Задачи» открыта AURORA-1620, в окне — переход по связи на AURORA-1621, нажата Esc
- **THEN** окно закрыто, адрес — адрес списка с прежними фильтрами; «назад» браузера не открывает карточку
#### Scenario: Прямой адрес
- **WHEN** в новой вкладке открыт `/app/issues/AURORA-1620`, затем нажат ✕
- **THEN** под окном был список задач без фильтров; после закрытия адрес `/app/issues`, «назад» не открывает карточку
#### Scenario: Скрытые комментарии
- **WHEN** в карточке нажато «Скрыть» у комментариев, затем страница перезагружена и открыта другая карточка
- **THEN** комментарии в ней скрыты, видна кнопка «Показать» и число комментариев
### Requirement: Служебные заметки
Карточка задачи SHALL давать править служебные заметки: «вид работы» — выбор из значений `kind_labels` настроек
(и текущего значения, если его там нет, и пустого), «статус по факту» — текст с подсказками из ключей `status_labels`,
«сделано», «осталось», «заметка владельца» — многострочный текст. Сохранение SHALL отправлять
`PUT /issues/{key}/note` только с изменёнными полями и `updated_by: "web"`; очищенное поле отправляется как `null`.
После успеха SHALL показываться сохранённая заметка с временем и автором; при 422 — текст ошибки у формы без потери
введённого. Уход со страницы с несохранёнными правками SHALL требовать подтверждения. Заметки пишутся только в хаб,
в Jira приложение ничего не отправляет.
Карточка задачи SHALL показывать служебные заметки в режиме чтения: «Вид работы», «Статус по факту», «Сделано»,
«Осталось», «Заметка владельца» (пустые — «—») и время и автор последнего изменения; кнопка «Править» SHALL открывать
форму: «вид работы» — выбор из значений `kind_labels` настроек (и текущего значения, если его там нет, и пустого),
«статус по факту» — текст с подсказками из ключей `status_labels`, «сделано», «осталось», «заметка владельца» —
многострочный текст; «Отмена» SHALL возвращать режим чтения, при несохранённых правках — после подтверждения.
Сохранение SHALL отправлять `PUT /issues/{key}/note` только с изменёнными полями и `updated_by: "web"`; очищенное поле
отправляется как `null`. После успеха SHALL показываться режим чтения с сохранённой заметкой; список задач под окном SHALL
показывать новые «вид работы» и «статус по факту» без «Перечитать», доска под окном SHALL перечитать снимок
(`GET /board`) без потери раскладки и прокрутки; при 422 — текст ошибки у формы без
потери введённого. Закрытие окна, переход по связи и уход в другой раздел с несохранёнными правками SHALL требовать
подтверждения. Заметки пишутся только в хаб, в Jira приложение ничего не отправляет.
#### Scenario: Только изменённые поля
- **WHEN** в заметке изменено только «осталось» и нажато «Сохранить»
- **THEN** тело `PUT /issues/{key}/note` — `{rest_text: "…", updated_by: "web"}`
- **WHEN** в заметке нажато «Править», изменено только «осталось» и нажато «Сохранить»
- **THEN** тело `PUT /issues/{key}/note` — `{rest_text: "…", updated_by: "web"}`; карточка снова в режиме чтения с новым
«Осталось»
#### Scenario: Уход с несохранёнными правками
- **WHEN** в форме заметки есть несохранённая правка и выбран раздел «Доска»
- **THEN** приложение спрашивает подтверждение; при отказе остаётся в карточке с введённым текстом
- **WHEN** в форме заметки есть несохранённая правка и нажата Esc
- **THEN** приложение спрашивает подтверждение; при отказе окно открыто, форма с введённым текстом
#### Scenario: Заметка видна на доске
- **WHEN** карточка AURORA-9002 открыта с доски, вид работы изменён на «тестирование» и сохранён
- **THEN** доска под окном остаётся на месте и перезапрашивается (`GET /board` отдаёт `work_kind` «тестирование»); окно закрывается без потери раскладки и прокрутки доски
### Requirement: Сборка и разработка
Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и