From e76b136e3748d64896baadd547e033c7f86f4553 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 17 Oct 2026 11:15:43 +0400 Subject: [PATCH] =?UTF-8?q?OpenSpec:=20hide-closed=20=E2=80=94=20=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D1=8B=D1=82=D1=8C=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B?= =?UTF-8?q?=D1=82=D1=8B=D0=B5=20=D0=B2=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B5,=20=D0=BF=D1=80=D0=B8=D0=B3=D0=BB=D1=83=D1=88=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BD=D0=B0=20=D0=B4=D0=BE=D1=81=D0=BA=D0=B5,?= =?UTF-8?q?=20=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D1=83=D1=81=20=D0=BF=D0=BE=20=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D1=83=20=D0=B8=D0=B7=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/hide-closed/.openspec.yaml | 2 + openspec/changes/hide-closed/design.md | 88 ++++++++ openspec/changes/hide-closed/proposal.md | 35 ++++ .../hide-closed/specs/data-model/spec.md | 30 +++ .../changes/hide-closed/specs/hub-api/spec.md | 69 +++++++ .../changes/hide-closed/specs/web-app/spec.md | 192 ++++++++++++++++++ 6 files changed, 416 insertions(+) create mode 100644 openspec/changes/hide-closed/.openspec.yaml create mode 100644 openspec/changes/hide-closed/design.md create mode 100644 openspec/changes/hide-closed/proposal.md create mode 100644 openspec/changes/hide-closed/specs/data-model/spec.md create mode 100644 openspec/changes/hide-closed/specs/hub-api/spec.md create mode 100644 openspec/changes/hide-closed/specs/web-app/spec.md diff --git a/openspec/changes/hide-closed/.openspec.yaml b/openspec/changes/hide-closed/.openspec.yaml new file mode 100644 index 0000000..b699b88 --- /dev/null +++ b/openspec/changes/hide-closed/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-17 diff --git a/openspec/changes/hide-closed/design.md b/openspec/changes/hide-closed/design.md new file mode 100644 index 0000000..573e0b7 --- /dev/null +++ b/openspec/changes/hide-closed/design.md @@ -0,0 +1,88 @@ +## Context + +`main` 9f4bb75. Устройство, на которое опираются правки: + +- **Настройки** — `src/jira_hub/settings.py`: `DEFAULTS` (ключ → значение по умолчанию), `ensure_defaults` при открытии + базы добавляет недостающие ключи (`INSERT OR IGNORE`), `validate` проверяет тип по значению по умолчанию, + `AGENT_KEYS` — что уходит агенту. `GET /settings` отдаёт всё, кроме токенов. +- **Список на сервере** — `api.ISSUE_FILTERS` → `mirror.list_issues(conn, filters, limit, offset)`: `_FILTERS` + (точное совпадение по колонке) плюс отдельные ветки для `label`, `neighbor`, `q`. +- **Список в приложении** — `web/src/issues/IssuesPage.tsx`: фильтры из query-строки по `ISSUE_FILTER_NAMES` + (`web/src/api/queries.ts`), все уходят в `GET /issues` как есть; `SELECTS` — выпадающие списки, варианты — + `filterOptions` (`web/src/issues/options.ts`), «только соседи» — флажок `neighbor=1`. Порции по 50 (`useIssues`). +- **Доска** — `web/src/board/`: модель из `GET /board` / CSV / JSON → видимая выборка (`analysis/visible.ts`, `VisNode` + с `dim` — сосед, класс карточки `out`) → `BoardCanvas` → `Card.tsx` (класс `card`, `out`, `crit`, `focus`) и + `Edges.tsx` (`` на каждую связь). Настройки — `useSettings()` в `BoardPage.tsx`, словари — `dicts.ts`. + Golden-тесты сравнивают модель, выборку, критический путь и проблемы с `links.html@3931a88`. +- **Карточка** — `web/src/issues/IssueCard.tsx`: шапка с бейджем статуса по факту (`factClass` по `status_labels`), + «просрочено» — `isOverdue(due, fact, statusLabels, today)` (`card.ts`, R-2 #37). `Notes.tsx` — строки заметки, + `NoteForm.tsx` — форма, поле `fact_status` с подсказками из `status_labels`; сохраняются только изменённые поля. +- **Живые данные** (17.10.2026): закрытые статусы AURORA — «Готово» и «Снято»; `transitions_map` и `status_labels` + в настройках ключуются статусами по факту — это касается будущего полного удаления, не этой задачи. + +## Goals / Non-Goals + +**Goals:** в списке по умолчанию только незакрытые задачи, закрытые — одним флажком; на доске закрытые не отвлекают, но +связи через них видны; «статуса по факту» в веб-приложении нет. + +**Non-Goals:** см. proposal.md. + +## Decisions + +1. **Набор закрытых статусов — настройка хаба `closed_statuses`**, список строк, по умолчанию `["Готово", "Снято"]`. + Приложение по правилу «Справочники из настроек хаба» своих копий словарей не держит, а сервер нужен для фильтра + списка. В `DEFAULTS`, поэтому на живом хабе ключ появится при первом открытии базы после выкатки (`ensure_defaults`); + тип проверяет `validate` (список). В `AGENT_KEYS` не входит. Сравнение — точное совпадение строки статуса. +2. **Фильтр списка — на сервере:** `GET /issues?hide_closed=1` добавляет условие `i.status IS NULL OR i.status NOT IN + (…closed_statuses)` (задача без статуса — не закрыта). Клиентский фильтр поверх порций по 50 ломал бы «Показать + ещё» (порция могла бы прийти пустой). Значение `1` / `true` — включено, иное или отсутствие — выключено (как + `neighbor`). Пустой `closed_statuses` — фильтр ничего не скрывает. +3. **Адрес списка:** по умолчанию закрытые скрыты, в адресе ничего нет; снятый флажок «скрыть закрытые» — `closed=1`. + Приложение отправляет `hide_closed=1`, пока `closed` не `1`. `closed` — параметр приложения, в `GET /issues` не + уходит. Флажок — рядом с «только соседи». +4. **Явный выбор статуса сильнее флажка:** если в фильтре «Статус Jira» выбран статус из `closed_statuses`, + `hide_closed` не отправляется — иначе выборка всегда пуста. Флажок при этом остаётся как есть. +5. **Доска:** признак закрытости вычисляется при отрисовке, а не в модели — модель, выборка, критический путь и + проблемы остаются как у `links.html` (golden-тесты не трогаются). Карточка получает класс `closed` + (`opacity: .4`), под указателем и с фокусом внутри — полная яркость (`.card.closed:hover`, `.card.closed:focus-within`; + признак `VisNode.focus` нигде не включается, выделения карточек на доске нет). Стрелка, у которой хотя бы один конец — + закрытая задача, — класс/атрибут с той же прозрачностью. Сосед (`out`) и закрытая — признаки независимые, вместе + — оба стиля. Набор закрытых — из `useSettings()`; карточки из CSV и JSON приглушаются по тому же правилу (статус у + них есть). +6. **Нет `closed_statuses` в настройках** (хаб старее приложения) — ничего не считается закрытым: список без фильтра, + доска без приглушения, ошибки «нет словаря» нет. Хаб и приложение выкатываются вместе, это окно короткое. +7. **«Просрочено» в карточке** — по статусу Jira: срок раньше сегодня и статус не из `closed_statuses` (было — по + классу статуса по факту, R-2 #37). Без статуса по факту прежнее правило не на чем считать. +8. **«Статус по факту» из приложения:** убрать из `ISSUE_FILTER_NAMES` и `SELECTS`, колонку таблицы, вариант из + `filterOptions`, бейдж в шапке карточки (`factClass`, стили `.badge.fact*`), строку в `Notes.tsx`, поле и подсказки + в `NoteForm.tsx`. Тип `NoteFields` / `NOTE_FIELDS` приложения — без `fact_status`; так форма не может отправить его + и в `PUT` значение не трогается. Тип ответа API (`IssueRow.fact_status`, `Note.fact_status`) — поле остаётся + необязательным, приложение его не читает. Старый адрес с `fact_status=` — параметр игнорируется (нет в + `ISSUE_FILTER_NAMES`). + +## Risks / Trade-offs + +- Новый статус Jira закрытого смысла (например, «Отклонено») не будет скрыт, пока его не добавят в `closed_statuses` + — осознанно: владелец назвал ровно два статуса. +- Пока хаб не перезапущен после выкатки, сборка приложения новее хаба: `hide_closed` хаб старой версии игнорирует, и + список покажет закрытые — безвредно. + +## Decisions made without discussion + +- 2026-10-17 — **набор закрытых статусов в настройке хаба `closed_statuses`**, а не константой в коде. Обоснование: + правило «Справочники из настроек хаба», фильтр нужен и серверу, и приложению. Цена ошибки: лишний ключ настроек; + убрать — правка `DEFAULTS` и двух мест чтения. +- 2026-10-17 — **фильтр `hide_closed` на сервере**, а не в браузере. Обоснование: порции по 50. Цена ошибки: один + параметр API; поведение MCP не меняется. +- 2026-10-17 — **адрес: `closed=1` — показать закрытые; по умолчанию параметра нет.** Обоснование: умолчание владельца + «скрыты», адрес без лишних параметров. Цена ошибки: переименование параметра. +- 2026-10-17 — **выбранный в «Статус Jira» закрытый статус отменяет скрытие.** Обоснование: иначе выборка всегда пуста. + Цена ошибки: поведение одного сочетания фильтров. +- 2026-10-17 — **приглушение: `opacity .4`, полная яркость под указателем и с фокусом клавиатуры внутри; стрелки — если закрыт хотя бы один + конец.** Обоснование: владелец подтвердил «связи тоже бледнеют»; степень прозрачности — на глаз, отличается от + соседа (`.7`, пунктир). Цена ошибки: правка CSS. +- 2026-10-17 — **без `closed_statuses` — ничего не закрыто, без сообщения об ошибке.** Обоснование: это не словарь + подписей, без него страница работает как раньше. Цена ошибки: нет. +- 2026-10-17 — **«просрочено» — по статусу Jira из `closed_statuses`.** Обоснование: статус по факту из приложения + убран. Цена ошибки: пометка «просрочено» у задачи, закрытой по факту, но не в Jira, — теперь видна (это и есть + статус Jira). diff --git a/openspec/changes/hide-closed/proposal.md b/openspec/changes/hide-closed/proposal.md new file mode 100644 index 0000000..a214257 --- /dev/null +++ b/openspec/changes/hide-closed/proposal.md @@ -0,0 +1,35 @@ +## Why + +В списке «Задачи» и на доске видны в том числе закрытые задачи: они занимают место и мешают видеть то, что ещё в +работе. Кроме того, «статус по факту» из служебных заметок вносит шум: в списке и карточке рядом два статуса. + +Решения владельца 17.10.2026 (brainstorming, jira-hub#57): +- закрытая задача — та, у которой **статус Jira** «Готово» или «Снято»; других признаков закрытости нет; +- список: закрытые можно **скрыть** — переключатель в фильтрах, по умолчанию они скрыты, выбор хранится в адресе, как + остальные фильтры; +- доска: закрытые **приглушены** — карточка и её связи полупрозрачные, всегда, без переключателя; карточка под + указателем или с фокусом клавиатуры внутри — в полную яркость (выделения карточек на доске нет); +- «статус по факту» убрать **из веб-приложения** (колонка и фильтр списка, шапка карточки, заметки в карточке и форма); + в хабе (база, API, MCP, `status_labels`, `transitions_map`) пока остаётся; полностью от него избавиться — позже, + отдельной задачей. + +## What Changes + +- **Настройки хаба:** новый ключ `closed_statuses` — список имён статусов Jira, считающихся закрытыми, по умолчанию + `["Готово", "Снято"]`; агенту не передаётся. +- **API хаба:** `GET /issues` принимает фильтр `hide_closed=1` — без задач, у которых `status` из `closed_statuses`. +- **Веб-приложение, список «Задачи»:** флажок «скрыть закрытые», по умолчанию включён; выключенный — `closed=1` в + адресе; колонки и фильтра «Статус по факту» нет. +- **Веб-приложение, доска:** карточки задач со статусом из `closed_statuses` и стрелки, у которых хотя бы один конец — + такая задача, приглушены; модель, видимая выборка, критический путь и проблемы не меняются. +- **Веб-приложение, карточка задачи:** в шапке нет статуса по факту; «просрочено» — по статусу Jira (срок прошёл и + статус не из `closed_statuses`); в «Служебных заметках» и их форме нет поля «Статус по факту» (сохранение его не + трогает). + +## Non-Goals + +- Удаление `fact_status` из базы, API `GET /issues` (фильтр и поле), MCP `issues` (колонка), `status_labels`, + `transitions_map`, импорта — отдельная задача после этой. +- Переключатель приглушения на доске, скрытие закрытых с доски. +- Правка `closed_statuses` из веб-приложения (меняется через `PUT /settings` / MCP `settings_set`). +- MCP `issues` не получает параметр `hide_closed`. diff --git a/openspec/changes/hide-closed/specs/data-model/spec.md b/openspec/changes/hide-closed/specs/data-model/spec.md new file mode 100644 index 0000000..7661fa5 --- /dev/null +++ b/openspec/changes/hide-closed/specs/data-model/spec.md @@ -0,0 +1,30 @@ +## MODIFIED Requirements + +### Requirement: Настройки +Таблица `setting` SHALL хранить парами ключ–JSON: `base_jql`, `field_ids`, `issue_types`, `priority_map`, `link_types`, +`transitions_map`, `pacing`, `board_id`, `tokens` (агентский и клиентский), `settings_version` (увеличивается при любой +правке), `backup_keep`, `auto_refresh_minutes`, `relay_dir` (абсолютный путь папки реле или `null`, по умолчанию `null`; +относительный путь и не-строка при записи — 422; агенту не передаётся), `closed_statuses` (список имён статусов Jira, считающихся закрытыми, по умолчанию +`["Готово", "Снято"]`; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением +по умолчанию при открытии хаба). Спринты агент читает с доски `board_id`, +отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json` +командой `jira-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект +`{task, epic, bug}`, у каждого `{name, id}` (`id` может быть `null`); `init` берёт их из ключей конфига +`issue_type_<тип>` и `issue_type_<тип>_id`; `bug` попадает в `issue_types`, только если в конфиге есть хотя бы +один из ключей `issue_type_bug`, `issue_type_bug_id` (`task` и `epic` — как раньше, всегда). + +#### Scenario: Правка настроек поднимает версию +- **WHEN** через API изменён `pacing` +- **THEN** `settings_version` увеличен, следующий `claim` отдаёт агенту новую версию + +#### Scenario: Конфиг без ключей ошибки +- **WHEN** `jira-hub init --config` с `issue_type_task`, `issue_type_epic`, без `issue_type_bug` +- **THEN** `issue_types` содержит `task` и `epic`, `bug` нет; постановка `ensure_issue` с `issue_type = bug` — 422 + +#### Scenario: Неверный relay_dir +- **WHEN** `PUT /settings` с `relay_dir = "rshb/jira-bridge"` +- **THEN** 422, значение не записано, `settings_version` не изменился + +#### Scenario: Закрытые статусы по умолчанию +- **WHEN** хаб открыл базу, в которой нет ключа `closed_statuses` +- **THEN** `GET /settings` отдаёт `closed_statuses = ["Готово", "Снято"]`; `claim` агенту этого ключа не передаёт diff --git a/openspec/changes/hide-closed/specs/hub-api/spec.md b/openspec/changes/hide-closed/specs/hub-api/spec.md new file mode 100644 index 0000000..dd642c2 --- /dev/null +++ b/openspec/changes/hide-closed/specs/hub-api/spec.md @@ -0,0 +1,69 @@ +## MODIFIED Requirements + +### Requirement: Маршруты интерфейсов +Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном: +- `GET /issues` с фильтрами `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, + `q` (подстрока темы), `hide_closed` (`1` / `true` — без задач, чей `status` входит в настройку `closed_statuses`; + задача без статуса не закрыта), `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` (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы), а также + по проверкам постановки из `commands` (тип `bug` без настройки, файл вложения не загружен). +- `POST /files` `{content}` (`content` — base64 содержимого файла) → `{file, size}`, где `file` — sha256 содержимого + (64 hex-символа, нижний регистр); см. «Хранилище файлов вложений». +- `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 = "Тестовый эпик"` + +#### Scenario: Без закрытых +- **WHEN** `closed_statuses = ["Готово", "Снято"]`, в зеркале задачи в статусах «В работе», «Готово», «Снято» и без + статуса; `GET /issues?hide_closed=1` +- **THEN** в ответе задачи «В работе» и без статуса; без `hide_closed` — все четыре diff --git a/openspec/changes/hide-closed/specs/web-app/spec.md b/openspec/changes/hide-closed/specs/web-app/spec.md new file mode 100644 index 0000000..1c58b56 --- /dev/null +++ b/openspec/changes/hide-closed/specs/web-app/spec.md @@ -0,0 +1,192 @@ +## MODIFIED Requirements + +### Requirement: Справочники из настроек хаба +Приложение SHALL брать перевод меток видов работ (`kind_labels`) и типов связей Jira (`link_types`) из `GET /settings` +и набор закрытых статусов Jira (`closed_statuses`) из `GET /settings` +и не содержать собственных копий этих словарей. Если нужного словаря в настройках нет, страница SHALL сообщать, какого +словаря не хватает, вместо пустой доски или списка без подписей; исключение — `closed_statuses`: без него ни одна +задача не считается закрытой, сообщения нет. + +#### Scenario: Новый тип связи в настройках +- **WHEN** в `link_types` настроек добавлен тип и в зеркале есть связь этого типа +- **THEN** после перезагрузки страницы доска показывает связь этого типа без изменения кода приложения + +### Requirement: Доска связей +Страница `/app/board` SHALL воспроизводить поведение доски связей хаба (`links.html` ревизии 3931a88) без изменений, +кроме перечисленных в этом требовании и в требовании «Источник данных доски»: построение модели из `GET /board`, CSV +фильтра Jira и JSON доски (`arm-ka-links/1`); дорожки по эпикам со свёрткой и перестановкой, служебные дорожки внизу; +карточки с перетаскиванием, растяжкой, размером по содержимому и привязкой к сетке; стрелки связей с выравниванием; +фильтры по спринтам и исполнителям; легенда со скрытием видов работ и типов связей; «+ соседи по связям», «⇄ передачи», +«скрыть без связей», «⚑ критический путь», «▥ по датам» с колонками дат и их растяжкой; ящик «Проблемы»; таблица +связей; экспорт и импорт JSON; тема; «Сбросить раскладку». Раскладка, дорожки, фильтры и тема SHALL храниться в +`localStorage` под теми же ключами `graph-<имя>…`, что у `links.html`, с той же миграцией старых ключей. +Результат построения модели, видимой выборки, критического пути и проблем SHALL совпадать с результатом `links.html` +на одних и тех же данных и фильтрах. У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку», +открывающий `/app/issues/{key}` окном поверх доски; ключ карточки по-прежнему открывает задачу в Jira. Пока окно открыто +и после его закрытия доска SHALL сохранять прокрутку, раскладку, фильтры и строку состояния пакета. Действия «Сменить токен» в меню +доски нет — оно в верхней панели приложения. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку +карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке +захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет. +Карточка задачи, статус Jira которой входит в `closed_statuses`, SHALL быть приглушена (полупрозрачна), под +указателем и с фокусом клавиатуры внутри карточки — в полную яркость; стрелка связи, у которой хотя бы один конец — такая задача, SHALL быть приглушена так же. +Приглушение — только вид: модель, видимая выборка, критический путь, проблемы, раскладка и экспорт JSON от него не +зависят; переключателя нет; правило одно для данных хаба, 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** доска показана с той же прокруткой + +#### Scenario: Закрытые на доске приглушены +- **WHEN** на доске AURORA-1620 «В работе», AURORA-1621 «Готово», AURORA-1622 «Снято», связь AURORA-1620 → AURORA-1621 +- **THEN** карточки AURORA-1621 и AURORA-1622 и стрелка AURORA-1620 → AURORA-1621 приглушены, AURORA-1620 — нет; + под указателем AURORA-1621 — в полную яркость; критический путь и ящик «Проблемы» те же, что без приглушения + +### Requirement: Список задач +Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (открывает карточку задачи окном поверх списка), тема, +тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы, +признак «сосед»; статуса по факту в списке нет. Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид +работы, статус Jira, метка, эпик, только соседи, поиск по ключу или теме, скрыть закрытые), храниться в query-строке +адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`; значение фильтра из адреса, которого нет среди +вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Флажок «скрыть закрытые» SHALL +быть включён по умолчанию (в адресе ничего), снятый — `closed=1` в адресе; включённый — запрос с `hide_closed=1`, +кроме случая, когда в фильтре «Статус Jira» выбран статус из `closed_statuses`. Параметр `fact_status` в адресе +игнорируется. Задачи 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** адрес списка с тем же фильтром, в таблице те же строки, прокрутка та же + +#### Scenario: Закрытые скрыты по умолчанию +- **WHEN** открыт `/app/issues`, в зеркале задачи в статусах «В работе», «Готово», «Снято» +- **THEN** флажок «скрыть закрытые» включён, запрос `GET /issues` с `hide_closed=1`, в таблице только «В работе»; после + снятия флажка адрес содержит `closed=1`, в таблице все три + +#### Scenario: Выбран закрытый статус +- **WHEN** флажок «скрыть закрытые» включён и в «Статус Jira» выбрано «Готово» +- **THEN** запрос `GET /issues?status=Готово` без `hide_closed`, в таблице задачи в статусе «Готово» + +### Requirement: Карточка задачи +Адрес `/app/issues/{key}` SHALL открывать карточку задачи из `GET /issues/{key}` модальным окном поверх страницы, с +которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры, +загруженные строки, прокрутку и раскладку. Перезагрузка страницы с открытым окном SHALL сохранять страницу под окном. Открытый напрямую +адрес (новая вкладка, закладка) SHALL +показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону (нажатие и отпускание — на фоне; выделение текста, +отпущенное за краем окна, окно не закрывает) и +возвращать на страницу, с которой его открыли, даже после переходов по связям внутри окна; «назад» браузера после +закрытия MUST NOT открывать карточку снова. Переход по связанной задаче SHALL менять содержимое окна и адрес, «назад» +браузера — возвращать к прежней карточке. Пока окно открыто, фокус SHALL оставаться в окне, страница под ним не +прокручивается. + +Содержимое окна: +- шапка: ключ, статус Jira, «Открыть в Jira», ✕, тема; строка + «исполнитель · срок · спринт · эпик» без пустых частей, срок — `planned_end`, иначе `duedate`, с пометкой «просрочено», + если он раньше сегодняшней даты и статус Jira не входит в `closed_statuses`; эпик — темой + (`epic_summary`), иначе ключом; +- «Связи» по группам в порядке: «Зависит от» (входящие связи логических типов `блокирует начало` и `блокирует + завершение` из `link_types`, с меткой «начало» / «завершение»), «Блокирует» (исходящие тех же типов, с теми же метками), + «Связана с» (тип `связанные` в обе стороны), «Прочие» (тип Jira вне `link_types`: имя типа и направление «→» / «←»); + в строке — ключ (открывает связанную задачу в окне), тема и статус Jira, а если задачи нет в зеркале — ключ и «нет в + зеркале»; пустые группы не показываются, без связей — «связей нет»; +- «Описание» — текст, отрендеренный по требованию «Разметка Jira в карточке», с кнопкой «исходник» / «текст» в + заголовке раздела; длиннее 8 строк — свёрнуто с кнопкой «развернуть» / «свернуть» в обоих режимах; без описания + раздела нет; +- «Комментарии (N)» — новые сверху, автор, время, кнопка «исходник» / «текст» у каждого комментария, текст, + отрендеренный по требованию «Разметка Jira в карточке»; кнопка «Скрыть» / «Показать» сворачивает раздел, выбор 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** комментарии в ней скрыты, видна кнопка «Показать» и число комментариев + +#### Scenario: Просрочка закрытой задачи +- **WHEN** у AURORA-1620 срок 2026-10-01, сегодня 2026-10-17, статус Jira «Готово» +- **THEN** в строке под темой «срок 01.10.2026» без пометки «просрочено»; при статусе «В работе» — с пометкой + +### Requirement: Служебные заметки +Карточка задачи SHALL показывать служебные заметки в режиме чтения: «Вид работы», «Сделано», +«Осталось», «Заметка владельца» (пустые — «—») и время и автор последнего изменения; кнопка «Править» SHALL открывать +форму: «вид работы» — выбор из значений `kind_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"}`; карточка снова в режиме чтения с новым + «Осталось» + +#### Scenario: Уход с несохранёнными правками +- **WHEN** в форме заметки есть несохранённая правка и нажата Esc +- **THEN** приложение спрашивает подтверждение; при отказе окно открыто, форма с введённым текстом + +#### Scenario: Заметка видна на доске +- **WHEN** карточка AURORA-9002 открыта с доски, вид работы изменён на «тестирование» и сохранён +- **THEN** доска под окном остаётся на месте и перезапрашивается (`GET /board` отдаёт `work_kind` «тестирование»); окно закрывается без потери раскладки и прокрутки доски