Пакет веб-багов: карточка, лента, вики-разметка, состояние модулей (#124) #125
2
openspec/changes/web-bugfixes/.openspec.yaml
Normal file
2
openspec/changes/web-bugfixes/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-25
|
||||
198
openspec/changes/web-bugfixes/design.md
Normal file
198
openspec/changes/web-bugfixes/design.md
Normal file
@ -0,0 +1,198 @@
|
||||
## Context
|
||||
|
||||
25.10.2026 пять сабагентов сверили открытые follow-up issue хаба и агентов с main (hub 1405ccd). Каждый маркер получил
|
||||
статус с доказательством `file:line`. Итоги записаны комментариями в issue. Владелец выбрал первым пакет A — видимые
|
||||
пользователю ошибки веба (sdlc-hub#124). Brainstorming-вопросы владелец пропустил («делай сам… не хочу с ними
|
||||
сталкиваться в будущем»), поэтому все решения ниже приняты агентом. Они перечислены в «Decisions made without
|
||||
discussion».
|
||||
|
||||
Состав пакета: #42 M-1, M-2, M-4, M-5; #116; #103 С6 и метки `kind-*`; #56 M-4, M-5, M-6; #35 T4.1/M-15; #60 M-3;
|
||||
#64 M-6, T9.1; #86 T6.2, T6.3, M-4; #68 T3.3. Ссылки на строки ниже — по main 1405ccd.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
Цель — убрать перечисленные ошибки. Каждую закрепить тестом, который падает до правки: юнит или поведение компонента в
|
||||
vitest, для окна карточки дополнительно e2e. Сервер, API, MCP и агенты не меняются.
|
||||
|
||||
Не цели:
|
||||
- прочие маркеры исходных issue: тесты, стиль, доступность UI-кита (#68), квадратичный разбор (#56 M-2), фокус окна
|
||||
(#42 M-3), `100vh` окна (#42 M-6);
|
||||
- новые возможности.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Окно карточки
|
||||
|
||||
1. **Поиск списка не закрывает окно (#42 M-1).** Сейчас `IssuesPage.tsx:31-38` через 300 мс после ввода делает
|
||||
`setParams(..., {replace: true})`. Если за это время открыто окно карточки, запись заменяет адрес окна адресом списка,
|
||||
и окно закрывается.
|
||||
|
||||
Решение — перед открытием карточки со страницы «Задачи» (строка, ключ) несохранённый поиск записывается в адрес сразу,
|
||||
таймер снимается. Окно открывается поверх списка уже с этим поиском. Отложенная запись MUST NOT срабатывать, пока
|
||||
открыто окно.
|
||||
|
||||
Реализация на усмотрение исполнителя, например:
|
||||
- `IssuesPage` даёт таблице свою обёртку `open`, которая сначала делает flush;
|
||||
- или эффект проверяет, что текущий адрес браузера — сам список.
|
||||
|
||||
2. **Повторное закрытие не уводит назад дальше (#42 M-2).** `overlay.tsx:37-41`: `close()` при каждом вызове делает
|
||||
`navigate(-depth)`. Двойной щелчок по ✕ или фону, Esc сразу после щелчка — два шага назад.
|
||||
|
||||
Решение — закрытие выполняется один раз для одной записи истории окна. Повторные вызовы игнорируются, пока адрес не
|
||||
сменился. Признак — `location.key` записи, с которой начато закрытие.
|
||||
|
||||
3. **Подтверждение ухода с несохранённой заметкой (#42 M-4).** `NoteForm.tsx:82`: блок `role="alertdialog"` без имени,
|
||||
фокус в него не переводится. Решение:
|
||||
- блоку — `aria-label` «Уйти без сохранения заметки?»;
|
||||
- при появлении фокус переходит на «Остаться» (безопасное действие по умолчанию);
|
||||
- прокрутка к блоку остаётся.
|
||||
|
||||
4. **Связанная задача «нет в зеркале» и окно 404 (#42 M-5).**
|
||||
- В «Связях» (`IssueCard.tsx:123-125`) ключ задачи вне зеркала открывает задачу в Jira в новой вкладке (как «Открыть
|
||||
в Jira» в шапке) со значком внешней ссылки, а не окно хаба. Пометка «нет в зеркале» остаётся.
|
||||
- Окно 404 (прямой адрес, ключ из текста, гонка) показывает «Закрыть» (закрывает окно целиком, как ✕). Если окно
|
||||
открыто переходом из другой карточки (`depth ≥ 1`), добавляется «Назад» — `navigate(-1)` к прежней карточке.
|
||||
- То же «Назад» — у ошибки загрузки с «Повторить».
|
||||
|
||||
### Строка состояния
|
||||
|
||||
5. **Ответ опроса прежнего проекта отбрасывается (#116).** `ActivityProvider.tsx`:
|
||||
- эффект смены проекта (`:75-90`) сбрасывает `activeRef`/`active` и событие;
|
||||
- эффект опроса (`:126-154`) в том же коммите видит `active` из замыкания старого рендера и снова ставит событие
|
||||
`batch` (`:139`);
|
||||
- после этого `active = null`, событие больше никто не снимает.
|
||||
|
||||
Решение — эффект опроса обрабатывает ответ, только если отслеживаемый пакет на момент обработки тот же:
|
||||
`activeRef.current` совпадает с `active` из замыкания (по `id` и `project`). Иначе ответ игнорируется. `finish` по
|
||||
устаревшему ответу тоже не вызывается.
|
||||
|
||||
Тест — детерминированный, без нагрузки: ответ `/batches/{id}` приходит в том же такте, что и смена проекта; после
|
||||
этого событие модуля `null`. Существующий тест `activity.test.tsx:331` при этом стабилен (прогон в цикле ≥ 20 раз
|
||||
зелёный).
|
||||
|
||||
### Лента и карточка
|
||||
|
||||
6. **Текст комментария в ленте без разметки (#103 С6).** Новая чистая функция `wikiPlain(text, opts)` рядом с
|
||||
разборщиком (`issues/wiki/`) переводит вики-разметку в обычный текст тем же разбором `parseWiki`:
|
||||
- выделение и `{{моно}}` — содержимое;
|
||||
- ссылка — её текст (у `[адрес]` — адрес);
|
||||
- ключ задачи — ключ, `[~логин]` — «@логин», вложение — имя;
|
||||
- блоки — их текст через пробел, заголовок — его текст, `----` — пусто.
|
||||
|
||||
`eventText` для `comment` берёт `wikiPlain`, затем прежние «одна строка» и обрезку до 300 символов с «…». Модуль ленты
|
||||
импортирует функцию из модуля задач того же модуля jira — граница модулей веба не нарушается. Та же функция
|
||||
применяется к «Истории» карточки: она рисуется тем же `EventList`.
|
||||
|
||||
7. **Метки видов работ отдельно (#103).** В «Прочих полях» (`IssueCard.tsx:227`):
|
||||
- строка «Вид работы в Jira» — названия видов из `kind_labels`, чьи метки есть у задачи, через запятую, в порядке
|
||||
меток задачи;
|
||||
- строка «Метки» — остальные метки.
|
||||
|
||||
Строка с пустым значением, как и сейчас, не показывается. Сравнение меток — точное, как у доски (`dicts.ts`).
|
||||
Служебное поле заметки «Вид работы» (`work_kind`) не трогается. Это заметка хаба, а не метка Jira, и они могут
|
||||
расходиться — отсюда подпись «в Jira».
|
||||
|
||||
8. **Описание из пробелов (#56 M-4).** `IssueCard.tsx:79`: раздел «Описание» показывается, только если
|
||||
`description.trim()` не пуст.
|
||||
|
||||
### Вики-разметка (#56 M-5, M-6)
|
||||
|
||||
9. **`\#`.** `#` добавляется в `ESCAPABLE` (`inline.ts:21`). Строка `\# текст` не список (уже так — `LIST` требует `#`
|
||||
первым символом), а `\#` показывается как `#`.
|
||||
|
||||
10. **Макросы с параметрами.** `MACRO` (`parse.ts:17`) принимает параметры у всех трёх тегов:
|
||||
`\{(code|noformat|quote)(?::[^}]*)?\}`. Закрывающий тег — по имени без параметров, как сейчас.
|
||||
|
||||
11. **Пробелы после открывающего тега.** В `closeMacro` (`parse.ts:111`) остаток строки открывающего тега из одних
|
||||
пробелов в тело блока не попадает. Ведущие пробелы строк внутри блока сохраняются.
|
||||
|
||||
12. **`[~логин|текст]`.** В `bracket` (`inline.ts:149-152`) логин — часть до `|`, обрезанная. Получается «@логин»,
|
||||
подпись не показывается. Пустой логин или логин с пробелом — текст как есть, как сейчас.
|
||||
|
||||
13. **Непарная `[` в ячейке таблицы.** В `row` (`parse.ts:127-132`) `[` увеличивает глубину, только если дальше в
|
||||
строке есть парная `]`. Иначе `[` — обычный символ, и `|` делит ячейки.
|
||||
|
||||
### Список задач, доска, проекты
|
||||
|
||||
14. **Список ждёт настройки (#60 M-3).** Запрос `GET /issues` (`useIssues`) включается, когда запрос `/settings`
|
||||
завершён — успехом или ошибкой. Пока нет — «Загрузка…», без «По этим фильтрам задач нет». При ошибке настроек
|
||||
закрытых статусов нет, запрос уходит с `hide_closed=1`, как сейчас.
|
||||
|
||||
15. **Два быстрых переключения фильтра доски (#35 T4.1/M-15).** `useBoardPrefs.ts:39-54`: `toggle` и `toggleHidden`
|
||||
считают новое значение из `prefs` замыкания, и второй клик в том же батче теряет первый. Решение — последнее значение
|
||||
держится в `ref`, обновляемом синхронно вместе с `setPrefs`. Новое значение считается из `ref` и тем же значением
|
||||
пишется в `localStorage`. Чистота updater-функции сохраняется: StrictMode не пишет дважды.
|
||||
|
||||
16. **Фильтр спринтов не из-за эпика (#64 T9.1).** `fromHub.ts:30`: `hasSprint` — только по своим задачам, которые
|
||||
не эпики (`!isEpic(it) && !it.neighbor && !!it.sprint`), то есть по узлам доски.
|
||||
|
||||
17. **«Повторить» при ошибке загрузки проектов (#64 M-6).** `ProjectGate.tsx:11`: у сообщения «Проекты не загрузились»
|
||||
есть кнопка «Повторить», которая перезапрашивает `GET /projects` (`refetch` запроса проектов). Через контекст
|
||||
проекта или прямо — на усмотрение исполнителя.
|
||||
|
||||
### Вложения
|
||||
|
||||
18. **Прежняя ошибка не мигает после успешного повтора (#86 T6.2).** После успешного скачивания хук помнит «успех в
|
||||
этой карточке» для вложения. Строка (`AttachmentList.tsx:36`) не показывает серверный `state = failed`, пока
|
||||
данные карточки не перечитаны. Отметку снимает приход данных, где `state` вложения уже не `failed`, или новый
|
||||
`start`.
|
||||
|
||||
19. **401 при опросе скачивания — экран входа (#86 T6.3).** В `useAttachmentDownload` (`:74`, `:84`) `AuthError` идёт
|
||||
в `dropTokenOn(e)`, как в остальном приложении, а не текстом под вложением. `HubDownError` — текст «хаб не отвечает»
|
||||
под вложением с «Повторить» (требование «Ошибки хаба»).
|
||||
|
||||
20. **Порядок вложений — как у хаба (#86 M-4).** Хаб уже отдаёт вложения новыми сверху (`mirror.py:300-301`: по
|
||||
`created_at` UTC, затем `attachment_id`). Веб не пересортирует (`newestFirst` убирается из `AttachmentList`).
|
||||
`findAttachment` (`inline.ts:36-45`) при одинаковом имени берёт первое вложение в порядке хаба. Так выбор совпадает
|
||||
с `attachment_ids` хаба и MCP и не зависит от `Date.parse` строк Jira.
|
||||
|
||||
### UI-кит
|
||||
|
||||
21. **Фокус после «Очистить» (#68 T3.3).** `SearchInput` (`ui/inputs.tsx:22`): после нажатия «Очистить» фокус
|
||||
переводится в поле ввода (ref). Иначе кнопка исчезает вместе с фокусом.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **#116 — гонка в порядке эффектов.** Решение опирается на то, что эффект смены проекта стоит раньше эффекта опроса и
|
||||
синхронно меняет `activeRef`. Тест должен воспроизводить именно один коммит со сменой проекта и ответом опроса, иначе
|
||||
он проходит и без правки. Исполнитель проверяет RED.
|
||||
- **`wikiPlain` и разбор.** Плоский текст строится разбором карточки. Ошибка разбора в ленте даёт исходник — так же
|
||||
безопасно, как в карточке (рендер блока с ошибкой — исходник).
|
||||
- **Ссылка «нет в зеркале» в Jira.** Адрес Jira — тот же `JIRA_URL`, что у «Открыть в Jira». Задача может быть и вне
|
||||
Jira (опечатка в связи) — тогда Jira покажет свою 404, это приемлемо.
|
||||
- **Окно 404 и «Назад».** `depth` берётся из state окна. При прямом адресе `depth = 0` — только «Закрыть».
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
Все — 25.10.2026: владелец пропустил вопросы brainstorming («делай сам»). Рулинги R-n — с R-1 в пределах фичи.
|
||||
|
||||
- R-1 (design #1): при открытии карточки отложенный поиск записывается сразу, окно открывается поверх списка с ним.
|
||||
Обоснование: пользователь ввёл поиск и видит отфильтрованную строку, на которую щёлкнул, — потерять поиск хуже.
|
||||
Цена ошибки — вместо этого отбрасывать поиск: правка одной функции.
|
||||
- R-2 (design #2): повторное закрытие окна игнорируется до смены адреса. Цена ошибки — нет.
|
||||
- R-3 (design #3): фокус подтверждения ухода — на «Остаться». Обоснование: безопасное действие по умолчанию, данные не
|
||||
теряются от лишнего Enter. Цена ошибки — перенести фокус на другую кнопку.
|
||||
- R-4 (design #4): ключ связи «нет в зеркале» открывает Jira в новой вкладке, а не окно хаба с 404. Обоснование: в хабе
|
||||
про задачу ничего нет, полезное действие — посмотреть её в Jira. Цена ошибки — вернуть окно: одна ветка в `Links`.
|
||||
- R-5 (design #4): у окна 404 после перехода по связи — «Назад» (на шаг) и «Закрыть» (всё окно). Цена ошибки — убрать
|
||||
кнопку.
|
||||
- R-6 (design #5): #116 чинится в провайдере (ответ сверяется с текущим `activeRef`), тест не ослабляется. Цена ошибки
|
||||
— нет: гипотеза подтверждена чтением кода при сверке.
|
||||
- R-7 (design #6): лента показывает текст комментария без разметки по разбору карточки; «История» карточки — тоже.
|
||||
Альтернатива — показывать начало комментария отрендеренным — отвергнута: строка ленты однострочная. Цена ошибки —
|
||||
правка `eventText`.
|
||||
- R-8 (design #7): метки видов работ — отдельной строкой «Вид работы в Jira» с названиями видов, из «Меток» убраны.
|
||||
Альтернатива — просто скрыть. Отвергнута: метка вида — единственный след вида работы в Jira, и он может расходиться
|
||||
с заметкой. Цена ошибки — скрыть строку.
|
||||
- R-9 (design #12): `[~логин|текст]` показывается «@логин» без подписи. Jira такой формы не документирует, логин важнее
|
||||
подписи. Цена ошибки — показывать подпись.
|
||||
- R-10 (design #13): непарная `[` в строке таблицы — обычный символ. Цена ошибки — нет.
|
||||
- R-11 (design #14): при ошибке `/settings` список всё равно запрашивается (с `hide_closed=1`, закрытых статусов нет),
|
||||
а не блокируется. Обоснование: список полезнее пустого экрана, ошибку настроек покажет страница. Цена ошибки — нет.
|
||||
- R-12 (design #19): `HubDownError` при опросе скачивания остаётся текстом под вложением с «Повторить», отдельного
|
||||
экрана нет — данные карточки остаются на экране (требование «Ошибки хаба»). Цена ошибки — нет.
|
||||
- R-13 (design #20): веб берёт порядок вложений у хаба, а при одинаковых именах — первое в этом порядке. Обоснование:
|
||||
совпадение с `attachment_ids` хаба и MCP. Цена ошибки — вернуть сортировку на клиенте.
|
||||
- R-14 (proposal): в пакет не взяты #42 M-3 (возврат фокуса после закрытия окна), M-6 (`100vh`), доступность
|
||||
UI-кита #68 и производительность разбора #56 M-2. Это не ошибки поведения, они остаются в своих issue. Цена ошибки —
|
||||
следующий пакет.
|
||||
47
openspec/changes/web-bugfixes/proposal.md
Normal file
47
openspec/changes/web-bugfixes/proposal.md
Normal file
@ -0,0 +1,47 @@
|
||||
## Why
|
||||
|
||||
25.10.2026 владелец попросил сверить все открытые follow-up issue хаба и агентов с текущим кодом. В веб-приложении
|
||||
хаба подтвердились ошибки, которые пользователь может увидеть: окно карточки закрывается само или уводит назад по
|
||||
истории дальше нужного, лента показывает сырую разметку Jira, после смены проекта в строке состояния остаётся чужое
|
||||
событие пакета, вики-разметка в отдельных случаях ломает таблицу или остаётся исходником. Владелец с ними пока не
|
||||
сталкивался, но не хочет столкнуться в будущем. Это пакет A сверки (sdlc-hub#124).
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Окно карточки** (#42): отложенный поиск списка больше не закрывает только что открытое окно (M-1); повторное
|
||||
закрытие не уводит дальше назад по истории (M-2); подтверждение «Уйти без сохранения?» получает имя и фокус (M-4);
|
||||
связанная задача «нет в зеркале» ведёт в Jira, а не в окно 404, у окна 404 после перехода по связи есть «Назад»
|
||||
(M-5).
|
||||
- **Строка состояния** (#116): ответ опроса пакета прежнего проекта после смены проекта отбрасывается — событие не
|
||||
«залипает», тест `activity.test.tsx` перестаёт флейкать.
|
||||
- **Лента** (#103 С6): текст комментария — без вики-разметки Jira.
|
||||
- **Карточка** (#103, #56 M-4): метки видов работ (`kind_labels`) не смешиваются с прочими метками — отдельная строка
|
||||
«Вид работы в Jira»; описание из одних пробелов — раздела нет.
|
||||
- **Вики-разметка** (#56 M-5, M-6): `\#`, `{quote:…}`/`{noformat:…}` с параметрами, `[~логин|текст]`, пробелы после
|
||||
`{code}`, непарная `[` в ячейке таблицы.
|
||||
- **Список задач** (#60 M-3): запрос не уходит, пока не пришли настройки (`closed_statuses`) — нет лишнего запроса и
|
||||
мелькания «По этим фильтрам задач нет».
|
||||
- **Доска связей** (#35 T4.1/M-15, #64 T9.1): два быстрых переключения фильтра не теряют первое; фильтр спринтов не
|
||||
появляется из-за спринта у эпика.
|
||||
- **Проекты** (#64 M-6): «Повторить» при ошибке загрузки проектов.
|
||||
- **Вложения** (#86 T6.2, T6.3, M-4): после успешного повтора не мигает прежняя ошибка; 401 при опросе скачивания —
|
||||
экран входа; порядок вложений и выбор при одинаковых именах — как у хаба.
|
||||
- **UI-кит** (#68 T3.3): после «Очистить» в поиске фокус остаётся в поле.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Изменения API, схемы, MCP, агентов: всё чинится на клиенте, хаб уже отдаёт нужные данные.
|
||||
- Прочие маркеры исходных issue (тесты, стиль, доступность UI-кита из #68, производительность разбора вики-разметки
|
||||
#56 M-2, фокус окна #42 M-3) — остаются в своих issue.
|
||||
- Правки Jira из веба — веб по-прежнему только для просмотра (служебные заметки — данные хаба).
|
||||
|
||||
## Impact
|
||||
|
||||
- Спека `web-app`: требования «Список задач», «Карточка задачи», «Служебные заметки», «Разметка Jira в карточке»,
|
||||
«Проект в приложении», «Лента изменений», «Скачивание вложений в карточке», «Состояние и обновление модулей».
|
||||
- Код (только `web/src/`): `modules/jira/issues/` (страница, окно, карточка, заметки, вложения, вики-разметка),
|
||||
`modules/jira/feed/`, `modules/jira/board/`, `shell/activity/ActivityProvider.tsx`, `projects/ProjectGate.tsx`,
|
||||
`ui/inputs.tsx`.
|
||||
- Тесты: vitest рядом с кодом; e2e — сценарии окна карточки (`web/e2e/issues.spec.ts`).
|
||||
- После merge — комментарии в исходных issue (#35, #42, #56, #60, #64, #68, #86, #103) о закрытых маркерах; #116
|
||||
закрывается.
|
||||
519
openspec/changes/web-bugfixes/specs/web-app/spec.md
Normal file
519
openspec/changes/web-bugfixes/specs/web-app/spec.md
Normal file
@ -0,0 +1,519 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Список задач
|
||||
Страница `/app/issues` SHALL показывать задачи текущего проекта из `GET /issues?project=<текущий>` таблицей: ключ
|
||||
(открывает карточку задачи окном поверх списка) с пометкой «сосед» (относительно проекта), тема, статус Jira,
|
||||
исполнитель, срок (`planned_end`, иначе `duedate`; просроченный срок незакрытой задачи выделен), спринт, эпик (имя
|
||||
эпика, иначе ключ), вид работы, тип; статуса по факту в списке нет. Щелчок по строке (кроме ссылок и выделения текста)
|
||||
SHALL открывать карточку задачи так же, как ключ. Шапка страницы SHALL показывать число показанных задач.
|
||||
|
||||
Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид работы, статус Jira, метка, эпик,
|
||||
только соседи, поиск по ключу или теме, скрыть закрытые), храниться в query-строке адреса вместе с `project` и
|
||||
применяться сервером. На виду SHALL быть поиск, переключатель «Открытые | Все», переключатель «Только соседи» и
|
||||
кнопка «Фильтры» с числом выбранных выпадающих списков; шесть выпадающих списков (спринт, исполнитель, вид работы,
|
||||
статус Jira, метка, эпик) — в панели, которая раскрывается этой кнопкой; каждый выбранный список SHALL показываться
|
||||
снимаемой плашкой «<подпись>: <значение>» и под строкой фильтров должно быть действие «Сбросить», снимающее все шесть
|
||||
списков. Варианты выпадающих списков SHALL браться из снимка `GET /board?project=<текущий>`; значение фильтра из
|
||||
адреса, которого нет среди вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Положение «Открытые»
|
||||
SHALL быть по умолчанию (в адресе ничего), «Все» — `closed=1` в адресе; «Открытые» — запрос с `hide_closed=1`,
|
||||
кроме случая, когда в фильтре «Статус Jira» выбран статус из `closed_statuses`. Параметр `fact_status` в адресе
|
||||
игнорируется. Задачи SHALL загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя
|
||||
порция полная. Запрос `GET /issues` MUST NOT уходить, пока не завершён запрос настроек хаба (`closed_statuses`): до этого страница показывает «Загрузка…», а не «По этим фильтрам задач нет»; настройки не загрузились — запрос уходит как без закрытых статусов. Поиск SHALL записываться в адрес после паузы ввода; если до записи открыта карточка задачи, поиск SHALL записываться в адрес списка сразу, до открытия окна, и окно поверх списка с этим поиском MUST NOT закрываться отложенной записью.
|
||||
|
||||
#### Scenario: Фильтр в адресе
|
||||
- **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель
|
||||
- **THEN** адрес содержит `project=`, `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого
|
||||
адреса в новой вкладке показывает ту же выборку, кнопка «Фильтры» показывает 2, видны плашки спринта и исполнителя
|
||||
|
||||
#### Scenario: Снять фильтр плашкой
|
||||
- **WHEN** выбраны спринт и исполнитель, нажат ✕ на плашке спринта
|
||||
- **THEN** в адресе нет `sprint=`, `assignee=` остался, таблица перезапрошена
|
||||
|
||||
#### Scenario: Конец списка
|
||||
- **WHEN** вторая порция вернула 17 задач
|
||||
- **THEN** в таблице 67 задач, «Показать ещё» не показана
|
||||
|
||||
#### Scenario: Фильтр по закрытому спринту
|
||||
- **WHEN** открыт адрес `/app/issues?project=AWPCA&sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет
|
||||
- **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру
|
||||
|
||||
#### Scenario: Карточка поверх списка
|
||||
- **WHEN** список отфильтрован по спринту, прокручен и догружен «Показать ещё», открыта карточка задачи и окно закрыто
|
||||
- **THEN** адрес списка с тем же фильтром, в таблице те же строки, прокрутка та же
|
||||
|
||||
#### Scenario: Щелчок по строке
|
||||
- **WHEN** в таблице щёлкнута ячейка темы строки AURORA-1620
|
||||
- **THEN** адрес `/app/issues/AURORA-1620`, карточка открыта окном поверх списка
|
||||
|
||||
#### Scenario: Закрытые скрыты по умолчанию
|
||||
- **WHEN** открыт `/app/issues`, в зеркале задачи в статусах «В работе», «Готово», «Снято»
|
||||
- **THEN** выбрано «Открытые», запрос `GET /issues` с `hide_closed=1`, в таблице только «В работе»; после выбора «Все»
|
||||
адрес содержит `closed=1`, в таблице все три
|
||||
|
||||
#### Scenario: Выбран закрытый статус
|
||||
- **WHEN** выбрано «Открытые» и в «Статус Jira» выбрано «Готово»
|
||||
- **THEN** запрос `GET /issues?status=Готово` без `hide_closed`, в таблице задачи в статусе «Готово»
|
||||
|
||||
#### Scenario: Поиск перед открытием карточки
|
||||
- **WHEN** в поиске списка введено «свод» и меньше чем через 300 мс щёлкнута строка AURORA-1620
|
||||
- **THEN** окно карточки AURORA-1620 открыто и остаётся открытым; после его закрытия адрес списка содержит `q=свод`
|
||||
|
||||
#### Scenario: Закрытый статус в адресе до настроек
|
||||
- **WHEN** открыт `/app/jira/issues?project=AWPCA&status=Готово`, ответ `/settings` ещё не пришёл
|
||||
- **THEN** запросов `GET /issues` нет, показано «Загрузка…»; после ответа `/settings` (где «Готово» в `closed_statuses`) —
|
||||
один запрос `GET /issues?status=Готово` без `hide_closed`; сообщение «По этим фильтрам задач нет» не появлялось
|
||||
|
||||
### Requirement: Карточка задачи
|
||||
Адрес `/app/issues/{key}` SHALL открывать карточку задачи из `GET /issues/{key}` модальным окном поверх страницы, с
|
||||
которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры,
|
||||
загруженные строки, прокрутку и раскладку. Перезагрузка страницы с открытым окном SHALL сохранять страницу под окном. Открытый напрямую
|
||||
адрес (новая вкладка, закладка) SHALL
|
||||
показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону (нажатие и отпускание — на фоне; выделение текста,
|
||||
отпущенное за краем окна, окно не закрывает) и
|
||||
возвращать на страницу, с которой его открыли, даже после переходов по связям внутри окна; «назад» браузера после
|
||||
закрытия MUST NOT открывать карточку снова. Переход по связанной задаче SHALL менять содержимое окна и адрес, «назад»
|
||||
браузера — возвращать к прежней карточке. Пока окно открыто, фокус SHALL оставаться в окне, страница под ним не
|
||||
прокручивается. Повторное закрытие, пока окно закрывается (двойной щелчок по ✕ или фону, Esc сразу после щелчка), MUST NOT уводить дальше назад по истории браузера.
|
||||
|
||||
Содержимое окна:
|
||||
- шапка: ключ, статус Jira, «Открыть в Jira», ✕, тема; строка
|
||||
«исполнитель · срок · спринт · эпик» без пустых частей, срок — `planned_end`, иначе `duedate`, с пометкой «просрочено»,
|
||||
если он раньше сегодняшней даты и статус Jira не входит в `closed_statuses`; эпик — темой
|
||||
(`epic_summary`), иначе ключом;
|
||||
- «Связи» по группам в порядке: «Зависит от» (входящие связи логических типов `блокирует начало` и `блокирует
|
||||
завершение` из `link_types`, с меткой «начало» / «завершение»), «Блокирует» (исходящие тех же типов, с теми же метками),
|
||||
«Связана с» (тип `связанные` в обе стороны), «Прочие» (тип Jira вне `link_types`: имя типа и направление «→» / «←»);
|
||||
в строке — ключ (открывает связанную задачу в окне), тема и статус Jira, а если задачи нет в зеркале — ключ ссылкой на задачу в Jira (новая вкладка, значок внешней ссылки) и «нет в
|
||||
зеркале»; окно хаба такая ссылка не открывает; пустые группы не показываются, без связей — «связей нет»;
|
||||
- «Описание» — текст, отрендеренный по требованию «Разметка Jira в карточке», с кнопкой «исходник» / «текст» в
|
||||
заголовке раздела; длиннее 8 строк — свёрнуто с кнопкой «развернуть» / «свернуть» в обоих режимах; без описания
|
||||
(пустое или из одних пробелов и переводов строк) раздела нет;
|
||||
- «Вложения (N)» — новые сверху: значок по типу файла (картинка, таблица, PDF, прочее), имя, размер, «автор · дата» и
|
||||
кнопка скачивания (требование «Скачивание вложений в карточке»); без вложений раздела нет;
|
||||
- «Комментарии (N)» — новые сверху, автор, время, кнопка «исходник» / «текст» у каждого комментария, текст,
|
||||
отрендеренный по требованию «Разметка Jira в карточке», под текстом — вложения, на которые ссылается комментарий
|
||||
(`attachment_ids`): строка «плашек» с именем и размером, щелчок — скачивание; кнопка «Скрыть» / «Показать» сворачивает раздел, выбор SHALL
|
||||
запоминаться в браузере для всех карточек; без комментариев — «комментариев нет»;
|
||||
- «Служебные заметки» (требование «Служебные заметки»);
|
||||
- «Прочие поля» — непустые из: тип, приоритет, автор, «Вид работы в Jira» (названия видов из `kind_labels`, метки которых
|
||||
есть у задачи, через запятую), метки (кроме меток видов из `kind_labels`), плановое начало, обновлена в 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» без пометки «просрочено»; при статусе «В работе» — с пометкой
|
||||
|
||||
#### Scenario: Двойной щелчок по закрытию
|
||||
- **WHEN** со списка «Задачи» открыта AURORA-1620, в окне — переход по связи на AURORA-1621, по ✕ щёлкнули дважды подряд
|
||||
- **THEN** окно закрыто, адрес — адрес списка; запись истории перед списком не открыта
|
||||
|
||||
#### Scenario: Связанная задача не в зеркале
|
||||
- **WHEN** у AURORA-1620 связь на AURORA-1999, которой нет в зеркале
|
||||
- **THEN** в «Связях» строка «AURORA-1999 · нет в зеркале»; ключ — ссылка `<адрес Jira>/browse/AURORA-1999` в новой
|
||||
вкладке, окно карточки остаётся на AURORA-1620
|
||||
|
||||
#### Scenario: Задача не в зеркале после перехода
|
||||
- **WHEN** в окне AURORA-1620 щёлкнут ключ AURORA-1999 в тексте комментария, хаб ответил 404
|
||||
- **THEN** окно сообщает «Задачи AURORA-1999 нет в зеркале хаба» с кнопками «Назад» и «Закрыть»; «Назад» возвращает
|
||||
окно к AURORA-1620, «Закрыть» закрывает окно и возвращает на страницу под ним
|
||||
|
||||
#### Scenario: Метки вида работы
|
||||
- **WHEN** у задачи метки `AWPCA` и `kind-dev`, в `kind_labels` есть «разработка» → `kind-dev`
|
||||
- **THEN** в «Прочих полях» строки «Вид работы в Jira: разработка» и «Метки: AWPCA»
|
||||
|
||||
#### Scenario: Описание из пробелов
|
||||
- **WHEN** описание задачи — три пробела и два перевода строки
|
||||
- **THEN** раздела «Описание» нет
|
||||
|
||||
### Requirement: Служебные заметки
|
||||
Карточка задачи SHALL показывать служебные заметки в режиме чтения: «Вид работы», «Сделано»,
|
||||
«Осталось», «Заметка владельца» (пустые — «—») и время и автор последнего изменения; кнопка «Править» SHALL открывать
|
||||
форму: «вид работы» — выбор из значений `kind_labels` настроек (и текущего значения, если его там нет, и пустого),
|
||||
«сделано», «осталось», «заметка владельца» —
|
||||
многострочный текст; «Отмена» SHALL возвращать режим чтения, при несохранённых правках — после подтверждения.
|
||||
Сохранение SHALL отправлять `PUT /issues/{key}/note` только с изменёнными полями и `updated_by: "web"`; очищенное поле
|
||||
отправляется как `null`. После успеха SHALL показываться режим чтения с сохранённой заметкой; список задач под окном SHALL
|
||||
показывать новый «вид работы» без «Перечитать», доска под окном SHALL перечитать снимок
|
||||
(`GET /board`) без потери раскладки и прокрутки; при 422 — текст ошибки у формы без
|
||||
потери введённого. Закрытие окна, переход по связи и уход в другой раздел с несохранёнными правками SHALL требовать
|
||||
подтверждения; блок подтверждения — с доступным именем «Уйти без сохранения заметки?», при появлении фокус 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` «тестирование»); окно закрывается без потери раскладки и прокрутки доски
|
||||
|
||||
### Requirement: Разметка Jira в карточке
|
||||
Описание задачи и текст каждого комментария в карточке SHALL показываться отрендеренной вики-разметкой Jira (хаб
|
||||
хранит их как есть, в wiki) так, как их показывает Jira. Разбираются:
|
||||
- блоки: заголовки `h1.`–`h6.` в начале строки; списки — строки, начинающиеся (после необязательных пробелов) с
|
||||
маркеров `*`, `#` и пробела (уровень вложенности — числом маркеров, `*` — маркированный, `#` — нумерованный,
|
||||
смешанная вложенность `*#` — по последнему маркеру) или с одного `-` и пробела (маркированный, первый уровень);
|
||||
`--`, `*-` и прочие сочетания с `-` — не список; таблицы — строки, начинающиеся с
|
||||
`|`, ячейки `||заголовок||` и `|ячейка|` (`|` внутри `[текст|адрес]` ячейку не делит; `[` без парной `]` дальше в строке —
|
||||
обычный символ и на деление ячеек не влияет); `{code}` / `{code:параметры}` … `{code}` и `{noformat}` / `{noformat:параметры}` … `{noformat}` —
|
||||
моноширинный блок без разбора разметки внутри; `{quote}` / `{quote:параметры}` … `{quote}` и строка `bq. ` — цитата; открывающий тег
|
||||
блока — в начале строки, закрывающий — в любом месте той же или последующей строки, текст после закрывающего тега
|
||||
разбирается дальше как обычные строки; пробелы после открывающего тега в той же строке в блок не попадают; тег не в начале строки — текст как есть; строка `----` —
|
||||
горизонтальная черта; прочие строки — абзацы, разделённые пустыми строками, одиночный перенос внутри абзаца —
|
||||
перенос строки;
|
||||
- внутри строки: `*жирный*`, `_курсив_`, `-зачёркнутый-`, `+подчёркнутый+`, `^верхний^`, `~нижний~`, `{{моно}}`
|
||||
(`{{…}}` — без разбора внутри); маркер выделения действует, только если открывающий стоит в начале текста или после
|
||||
пробела либо знака препинания и за ним не пробел и не тот же символ (`**x**` — текст как есть), а закрывающий стоит после не-пробела и за ним не буква и не
|
||||
цифра (`data_type`, `2026-10-15`, `счёт-фактура`, `a*b*c` — обычный текст); `\\` — перенос строки; `\` перед
|
||||
символом разметки (в том числе `#`: `\#` — символ `#`) — сам символ;
|
||||
- ссылки: `[текст|адрес]`, `[адрес]` и голые адреса `http://`, `https://` — ссылка в новой вкладке, только для схем
|
||||
`http`, `https`, `mailto`, иначе текст как есть; `[ключ задачи]`, `[текст|ключ задачи]` и ключ задачи в тексте вида
|
||||
`ПРОЕКТ-123`, где `ПРОЕКТ` — `project_key` из настроек хаба, — ссылка, открывающая карточку этой задачи в окне
|
||||
(`UTF-8`, `SHA-256` и ключи других проектов — текст);
|
||||
`[~логин]` и `[~логин|текст]` — «@логин»;
|
||||
- ссылки на вложения задачи — `!имя!`, `!имя|параметры!`, `[^имя]`, `[текст|^имя]` с тем же разбором имени, что у хаба
|
||||
(`hub-api`, «Ссылки на вложения в тексте»), имя есть среди вложений задачи: ссылка-кнопка со значком скрепки и текстом
|
||||
— именем файла (у `[текст|^имя]` — текстом), щелчок — скачивание; картинка в тексте не показывается; имени нет среди
|
||||
вложений — исходник как есть.
|
||||
Всё, что разборщик не распознал (`{panel}`, `{color}`, `!картинка!` и `[^имя]` без такого вложения у задачи, незакрытые маркеры и блоки и прочее), SHALL
|
||||
показываться как текст исходника без потерь. Разметка MUST NOT превращаться в HTML строкой (`innerHTML`,
|
||||
`dangerouslySetInnerHTML`): текст задачи не может добавить на страницу элементы, атрибуты или скрипты. Если рендер
|
||||
блока завершился ошибкой, блок SHALL показываться исходником.
|
||||
|
||||
Кнопка «исходник» у описания и у каждого комментария SHALL переключать этот блок на исходник — текст с сохранёнными
|
||||
переносами строк, как без разбора разметки, — и обратно кнопкой «текст»; остальные блоки не переключаются. При каждом
|
||||
открытии карточки (в том числе при переходе по связи) все блоки показываются отрендеренными, выбор не запоминается.
|
||||
|
||||
#### Scenario: Описание со структурой
|
||||
- **WHEN** описание AURORA-1620 — `h3. Контекст`, строка `Шаги:`, затем `# Загрузить ОСВ` и `# Открыть документ`,
|
||||
затем пустая строка и `*Итог:* ошибка в {{data_type}}`
|
||||
- **THEN** в «Описании» заголовок «Контекст», абзац «Шаги:», нумерованный список из двух пунктов и абзац, где «Итог:» —
|
||||
жирным, «data_type» — моноширинным; символов `h3.`, `#`, `*`, `{{` не видно
|
||||
|
||||
#### Scenario: Переключение на исходник
|
||||
- **WHEN** в карточке AURORA-1620 у описания нажато «исходник»
|
||||
- **THEN** описание показано исходником с переносами строк (видны `h3. Контекст` и `# Загрузить ОСВ`), кнопка — «текст»;
|
||||
комментарии остаются отрендеренными; после «текст» описание снова отрендерено
|
||||
|
||||
#### Scenario: Исходник у одного комментария
|
||||
- **WHEN** у второго комментария нажато «исходник», затем открыта связанная задача и возврат «назад»
|
||||
- **THEN** сразу после нажатия исходником показан только второй комментарий; после возврата все комментарии
|
||||
отрендерены
|
||||
|
||||
#### Scenario: Ключ задачи в тексте
|
||||
- **WHEN** комментарий — `П. 1 ведётся в AURORA-1445; AURORA-1645 снята как дубль.`
|
||||
- **THEN** «AURORA-1445» и «AURORA-1645» — ссылки; щелчок по первой меняет содержимое окна на AURORA-1445
|
||||
|
||||
#### Scenario: Неизвестная разметка и опасный адрес
|
||||
- **WHEN** описание — `{color:red}важно{color}`, `!схема.png!` и `[нажми|javascript:alert(1)]`
|
||||
- **THEN** все три показаны текстом исходника как есть, ссылки и картинки нет, скрипт не выполняется
|
||||
|
||||
#### Scenario: Списки с отступом и пустыми строками
|
||||
- **WHEN** описание — `Шаги воспроизведения:`, ` * Загрузить ОСВ.`, пустая строка, ` * Дождаться обработки.`
|
||||
- **THEN** абзац «Шаги воспроизведения:» и маркированные пункты «Загрузить ОСВ.» и «Дождаться обработки.» без
|
||||
звёздочек
|
||||
|
||||
#### Scenario: Обычный текст не портится
|
||||
- **WHEN** описание — `Поле data_type = null, дата 2026-10-15, оборот 18 803 903,65 - 9 351 102,89, a*b*c, UTF-8,
|
||||
**важно**`
|
||||
- **THEN** текст показан как есть, без курсива, зачёркивания, жирного и ссылок
|
||||
|
||||
#### Scenario: Скриншот в тексте комментария
|
||||
- **WHEN** комментарий — «см. !скрин 1.png|thumbnail!», у задачи есть вложение «скрин 1.png»
|
||||
- **THEN** вместо `!скрин 1.png|thumbnail!` — кнопка со скрепкой «скрин 1.png»; под комментарием плашка «скрин 1.png ·
|
||||
234,4 КБ»; щелчок по любой из них скачивает файл
|
||||
|
||||
#### Scenario: Экранированная решётка и цитата с параметрами
|
||||
- **WHEN** описание — `\# не пункт`, затем `{quote:title=Итог}`, `согласовано`, `{quote}`
|
||||
- **THEN** абзац «# не пункт» (не нумерованный список) и цитата «согласовано»; тегов `{quote` не видно
|
||||
|
||||
#### Scenario: Таблица с непарной скобкой
|
||||
- **WHEN** описание — `|[draft|итог|готово|`
|
||||
- **THEN** таблица из одной строки с ячейками «[draft», «итог», «готово»
|
||||
|
||||
#### Scenario: Упоминание с подписью и код с пробелами
|
||||
- **WHEN** комментарий — `[~ivanov|Иванов]`, затем строка `{code} `, `x = 1`, `{code}`
|
||||
- **THEN** «@ivanov» и моноширинный блок из одной строки `x = 1` без пустой строки перед ней
|
||||
|
||||
### Requirement: Проект в приложении
|
||||
«Обзор», доска, список задач, лента и вики SHALL показывать один проект — текущий. Текущий проект SHALL задаваться
|
||||
параметром `project` в адресе страниц; без параметра — последний выбранный в этом браузере (`localStorage`), если он
|
||||
есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории.
|
||||
Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и подсказку выбрать
|
||||
проект в списке «Проект» боковой панели (на узком экране — в «Меню») вместо данных. Проектов нет — страницы SHALL
|
||||
показывать приглашение создать проект («+ Новый проект…»). Список проектов не загрузился — сообщение с ошибкой и кнопкой «Повторить», перезапрашивающей `GET /projects`. Переходы между страницами SHALL сохранять текущий проект;
|
||||
смена проекта на странице, которой нет у нового проекта, открывает «Обзор»; исключение — «Бэклог», «Спринт», «Kanban»:
|
||||
адрес остаётся, страница показывает «У проекта нет …-доски» (требование «Спринты и бэклог»). Карточка задачи
|
||||
(`/app/jira/issues/{key}`) от проекта не зависит.
|
||||
|
||||
Доска SHALL загружать `GET /board?project=<текущий>`. Раскладка, дорожки и фильтры доски в `localStorage` SHALL
|
||||
храниться отдельно для каждого проекта; у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов,
|
||||
сохраняется). Фильтр спринтов SHALL показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`), не
|
||||
эпика, есть спринт; иначе — «колонки «Спринт» нет — показаны все задачи».
|
||||
|
||||
Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка (с
|
||||
подсказкой «метка задач в Jira и имя проекта в хабе») и название, затем поля каждого модуля в своей группе с
|
||||
заголовком — названием модуля: «Jira» — номер доски Jira (необязательно; подсказка «scrum или kanban определится
|
||||
сам»), «Вики» — корень вики (необязательно; подсказка «DOC-… или ссылка на страницу Eva»). Подсказка — текстом под
|
||||
полем, не в placeholder. Пустое необязательное поле в запрос не попадает; числовое поле с нецелым значением — ошибка у
|
||||
поля, запрос не уходит. «Создать» — `POST /projects` с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с
|
||||
кнопкой, окно остаётся открытым; успех — окно закрывается, текущим становится новый проект, каждый пакет из ответа
|
||||
(`batches`) отслеживается своим модулем (требование «Состояние и обновление модулей»).
|
||||
|
||||
#### Scenario: Первый вход после выкатки
|
||||
- **WHEN** открыт `/app/jira/links` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK`
|
||||
- **THEN** адрес — `/app/jira/links?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек
|
||||
|
||||
#### Scenario: Переключение проекта
|
||||
- **WHEN** на доске `AWPCA` в выпадающем списке выбран `STOCKS_DRRK`, затем открыт раздел «Задачи»
|
||||
- **THEN** доска — `GET /board?project=STOCKS_DRRK` со своей раскладкой; адрес «Задач» —
|
||||
`/app/jira/issues?project=STOCKS_DRRK`
|
||||
|
||||
#### Scenario: Неизвестный проект в адресе
|
||||
- **WHEN** открыт `/app/jira/issues?project=NOPE`
|
||||
- **THEN** «проект NOPE не найден» и подсказка выбрать проект в боковой панели; запроса `GET /issues` нет
|
||||
|
||||
#### Scenario: Создание проекта из формы
|
||||
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», доска и корень вики пусто; нажато «Создать»
|
||||
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", author: "viewer"}`; текущий проект — `STOCKS_DRRK`;
|
||||
строка «Jira» блока состояния показывает «пакет #N выполняется»
|
||||
|
||||
#### Scenario: Проект с корнем вики из формы
|
||||
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», корень `DOC-001234`; нажато «Создать»
|
||||
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234", author: "viewer"}`; строка
|
||||
«Jira» показывает пакет обновления, строка «Вики» — пакет синхронизации
|
||||
|
||||
#### Scenario: Метка занята
|
||||
- **WHEN** в форме введена метка `awpca`, хаб ответил 409
|
||||
- **THEN** окно открыто, рядом с «Создать» — текст ошибки хаба
|
||||
|
||||
#### Scenario: Канбан-проект
|
||||
- **WHEN** текущий проект — `STOCKS_DRRK`, ни у одной своей задачи снимка нет спринта (у соседа из `AWPCA` спринт есть)
|
||||
- **THEN** вместо фильтра спринтов — «колонки «Спринт» нет — показаны все задачи»
|
||||
|
||||
#### Scenario: Нецелый номер доски
|
||||
- **WHEN** в поле «Номер доски Jira» введено `51.5`, нажато «Создать»
|
||||
- **THEN** у поля — «нужно целое число», запроса `POST /projects` нет
|
||||
|
||||
#### Scenario: Спринт только у эпика
|
||||
- **WHEN** у эпика AURORA-9100 проекта спринт «AWPCA_Спринт 2», у остальных своих задач спринта нет
|
||||
- **THEN** вместо фильтра спринтов — «колонки «Спринт» нет — показаны все задачи»
|
||||
|
||||
#### Scenario: Проекты не загрузились
|
||||
- **WHEN** `GET /projects` ответил 500, затем нажато «Повторить», хаб ответил списком проектов
|
||||
- **THEN** сначала «Проекты не загрузились: …» с кнопкой «Повторить»; после нажатия — страница текущего проекта
|
||||
|
||||
### Requirement: Лента изменений
|
||||
Страница `/app/feed?project=<метка>` SHALL показывать события журнала изменений своих задач текущего проекта за период
|
||||
(`GET /changes` с `project`, `since`, `until`, `kinds`). Период SHALL выбираться переключателем «2 часа» / «Сегодня» /
|
||||
«Вчера» / «7 дней» (по умолчанию «Сегодня»; границы — по местному времени браузера: «Сегодня» — с начала текущих суток
|
||||
до сейчас, «Вчера» — с начала прошлых суток до начала текущих) и храниться в адресе (`period=2h|today|yesterday|7d`;
|
||||
«Сегодня» в адрес не пишется). Виды событий SHALL выбираться флажками «Статусы», «Описание», «Сроки», «Комментарии»,
|
||||
«Прочие поля» (по умолчанию все, кроме «Прочие поля») и храниться в адресе (`kinds=` через запятую; набор по умолчанию в
|
||||
адрес не пишется; в адресе нет ни одного известного вида — набор по умолчанию); снятые все флажки — сообщение «выберите
|
||||
хотя бы один вид», запроса нет.
|
||||
|
||||
События SHALL быть сгруппированы по задаче; задачи — по времени последнего события, новые сверху. У группы: ключ —
|
||||
ссылка на карточку задачи (окно поверх ленты, требование «Карточка задачи»), тема и текущий статус; под ней события по
|
||||
времени, новые сверху, каждое — время (`ЧЧ:ММ`, с датой `ДД.ММ`, если событие не в текущих сутках), автор и текст:
|
||||
статус — «статус «было» → «стало»»; описание — «описание изменено»; срок, плановое начало и окончание — «срок ДД.ММ →
|
||||
ДД.ММ» (дата по значению `from_value`/`to_value`, иначе строка журнала; пустое — «—»); комментарий — «комментарий:» и
|
||||
первые 300 символов текста без вики-разметки Jira (так, как текст читается отрендеренным: без `*`, `{code}`, `h3.`; у `[текст|адрес]` — текст, `[~логин]` — «@логин») одной строкой с «…»; прочие поля — «<название поля>: «было» → «стало»» (значения до 80
|
||||
символов с «…»), где название — подпись из словаря приложения (исполнитель, спринт, метки, приоритет, тема, эпик, связь,
|
||||
вложение, резолюция), иначе имя поля Jira. Подзаголовок страницы — число событий и границы периода. Событий нет —
|
||||
сообщение «за период изменений нет»; `truncated` — сообщение «показаны последние N событий — выберите период короче».
|
||||
Ошибка хаба — сообщение с кнопкой «Повторить». Ленту SHALL перечитывать завершение «Обновить из Jira» (вместе со списками
|
||||
задач) и возврат на вкладку браузера.
|
||||
|
||||
#### Scenario: Что закрылось за два часа
|
||||
- **WHEN** открыта лента проекта AWPCA, выбран период «2 часа», сняты все флажки, кроме «Статусы»
|
||||
- **THEN** адрес — `/app/feed?project=AWPCA&period=2h&kinds=status`; запрос `GET /changes?project=AWPCA&since=<сейчас − 2 ч>&kinds=status`;
|
||||
задачи сгруппированы, у каждой — события «ЧЧ:ММ автор: статус «В работе» → «Готово»»
|
||||
|
||||
#### Scenario: Карточка из ленты
|
||||
- **WHEN** в ленте нажат ключ AURORA-9002
|
||||
- **THEN** поверх ленты открыто окно карточки AURORA-9002; закрытие окна возвращает ленту с тем же периодом
|
||||
|
||||
#### Scenario: Пустой период
|
||||
- **WHEN** за выбранный период событий нет
|
||||
- **THEN** показано «за период изменений нет»
|
||||
|
||||
#### Scenario: Вчера
|
||||
- **WHEN** выбран период «Вчера», сейчас 22.10 14:05 местного времени
|
||||
- **THEN** запрос с `since` = 21.10 00:00 и `until` = 22.10 00:00 местного времени
|
||||
|
||||
#### Scenario: Комментарий с разметкой
|
||||
- **WHEN** в ленте событие комментария с текстом `h3. Итог` и `*Готово*, см. [протокол|https://x.ru/p] и [~ivanov]`
|
||||
- **THEN** строка события — «комментарий: Итог Готово, см. протокол и @ivanov»
|
||||
|
||||
### Requirement: Скачивание вложений в карточке
|
||||
Кнопка скачивания вложения (в разделе «Вложения», плашка под комментарием, ссылка в тексте) SHALL работать одинаково:
|
||||
1. `POST /attachments/{id}/fetch {author: "web"}`;
|
||||
2. `state = ready` — `GET /attachments/{id}/content` с клиентским токеном, браузер сохраняет файл под именем `filename`
|
||||
(через `Blob` и временную ссылку `download`; адрес с токеном в строке браузера MUST NOT появляться);
|
||||
3. `state = fetching` — у всех кнопок этого вложения в карточке вместо «Скачать» — «Загружается…» (неактивна), опрос
|
||||
`GET /attachments/{id}` раз в 2 с, пока окно открыто на этой задаче; `ready` — шаг 2 сам, без второго нажатия; закрыли
|
||||
окно или ушли на другую задачу — опрос прекращается, команда остаётся в очереди (файл ляжет в хаб);
|
||||
4. `failed` — под вложением в разделе «Вложения» текст ошибки и кнопка «Повторить» (новый запрос); 409 `agent_too_old`
|
||||
и прочие ошибки хаба — их текст там же; хаб не отвечает — «хаб не отвечает» там же; 401 на любом шаге — экран входа.
|
||||
После успешного повтора прежняя ошибка `failed` из данных карточки MUST NOT показываться.
|
||||
Вложения в разделе SHALL идти в порядке ответа хаба (новые сверху); ссылка в тексте на имя, которое носят несколько
|
||||
вложений, SHALL вести к первому из них в этом порядке (как `attachment_ids` хаба).
|
||||
Вложение с `state = ready` в разделе «Вложения» помечается «у хаба» и скачивается сразу. Размер неизвестен — «размер неизвестен» и обычная кнопка.
|
||||
Вложение больше 10 МБ —
|
||||
вместо кнопки ссылка «Открыть в Jira» (`<адрес Jira>/secure/attachment/<id>/<имя в URL-кодировке>`, новая вкладка) с
|
||||
подсказкой «больше 10 МБ — только в Jira»; плашка и ссылка в тексте ведут туда же.
|
||||
|
||||
#### Scenario: Скачать скриншот
|
||||
- **WHEN** в карточке AURORA-1650 нажато «Скачать» у «скрин 1.png», файла у хаба нет, агент онлайн
|
||||
- **THEN** кнопка — «Загружается…», через несколько секунд браузер сохраняет «скрин 1.png», кнопка снова «Скачать», у
|
||||
вложения отметка «у хаба»
|
||||
|
||||
#### Scenario: Окно закрыли во время скачивания
|
||||
- **WHEN** нажато «Скачать», затем окно карточки закрыто
|
||||
- **THEN** опросов `GET /attachments/{id}` больше нет; при следующем открытии карточки, если файл уже у хаба, вложение с
|
||||
отметкой «у хаба»
|
||||
|
||||
#### Scenario: Большой файл
|
||||
- **WHEN** у вложения «запись.mp4» размер 25 МБ
|
||||
- **THEN** вместо «Скачать» — ссылка «Открыть в Jira» с подсказкой «больше 10 МБ — только в Jira»
|
||||
|
||||
#### Scenario: Агент старой версии
|
||||
- **WHEN** нажато «Скачать», хаб ответил 409 «агент v13 не умеет скачивать вложения — нужен v14»
|
||||
- **THEN** под вложением этот текст и «Повторить»
|
||||
|
||||
#### Scenario: Повтор после ошибки
|
||||
- **WHEN** у вложения «скрин 1.png» в данных карточки `state = failed`, нажато «Повторить», файл скачан
|
||||
- **THEN** браузер сохранил файл, текста ошибки под вложением нет — и до, и после перечитывания карточки
|
||||
|
||||
#### Scenario: Токен отозван во время скачивания
|
||||
- **WHEN** нажато «Скачать», во время опроса `GET /attachments/{id}` хаб ответил 401
|
||||
- **THEN** приложение показывает экран входа
|
||||
|
||||
### Requirement: Состояние и обновление модулей
|
||||
Каждый веб-модуль SHALL описывать своё состояние данными, а рисовать их SHALL ядро. Описание модуля: название для
|
||||
показа («Jira», «Вики»); агент (имя модуля в `GET /agents`) или его отсутствие; свежесть данных для текущего проекта —
|
||||
время, охват («все проекты» или «проект») и ошибка последнего обновления; действие обновления (название, охват,
|
||||
постановка пакета) или его отсутствие; правило, какие пакеты принадлежат модулю; данные, которые надо перечитать после
|
||||
пакета модуля; есть ли у модуля данные для проекта (у вики без корня — нет).
|
||||
|
||||
Ядро SHALL:
|
||||
- опрашивать `GET /agents` при входе и раз в 60 с — один опрос на приложение; агент модуля: «на связи», «офлайн» или
|
||||
«обновите агента» (есть `warning` или `update_hint`, текст — в подсказке);
|
||||
- вести один трекер пакетов на приложение: на модуль — не больше одного отслеживаемого пакета; пакет подхватывается при
|
||||
входе, раз в 60 с и после действия обновления, если он не завершён, принадлежит модулю (поле `module` в `GET
|
||||
/batches`) и текущему проекту (для модулей с охватом «проект») — любого автора, в том числе автообновления хаба и
|
||||
MCP; опрос `GET /batches/{id}` каждые 3 с до терминального статуса; 4xx (404 — «пакета нет в хабе») и 5 ошибок «хаб
|
||||
не отвечает» подряд — опрос остановлен с сообщением; по завершении — перечитать данные модуля и список проектов; при смене проекта отслеживаемый пакет модуля с
|
||||
охватом «проект» сбрасывается вместе с событием, и ответ опроса этого пакета, пришедший после смены, MUST NOT
|
||||
возвращать событие;
|
||||
- ответ 409 на постановку пакета — отслеживать уже идущий пакет модуля, без сообщения об ошибке;
|
||||
- агент модуля офлайн при обновлении — пакет всё равно ставится, событие «агент офлайн — пакет выполнится, когда он
|
||||
выйдет на связь; пакет #N в очереди…».
|
||||
|
||||
Блок состояния в развёрнутой боковой панели — по строке на модуль, у которого есть данные для проекта: название модуля,
|
||||
точка агента (цвет и текст «агент на связи» / «агент офлайн» / «обновите агента» — не только цвет), кнопка-значок
|
||||
обновления с подписью действия в подсказке и `aria-label` («Обновить из Jira — все проекты», «Обновить вики — проект
|
||||
AWPCA»); под ними — «данные на ДД.ММ ЧЧ:ММ» с охватом («все проекты» / «проект»), «обновления ещё не было» или ошибка
|
||||
последнего обновления; под ними — одно текущее событие пакета модуля: в очереди / выполняется (с оставшимся временем,
|
||||
если хаб его дал, и пометками «агент офлайн», «хаб не отвечает, повтор»), выполнен (исчезает через 10 с), не выполнен
|
||||
с текстом ошибки, отменён, опрос остановлен с причиной. Кнопка обновления модуля недоступна (с признаком загрузки), пока
|
||||
запрос постановки в пути или выполняется пакет действия обновления модуля (у jira — пакет с первой командой `refresh`, у
|
||||
wiki — `wiki_sync`); прочие пакеты модуля показываются, но обновление не блокируют: нажатие ставит пакет обновления, и
|
||||
трекер переключается на него. Свёрнутая панель — по значку модуля с точкой агента, подробности в
|
||||
подсказке, нажатие — обновление. На узком экране — сводная точка в верхней строке (требование «Оболочка и адреса»),
|
||||
событие пакета — строкой под верхней строкой.
|
||||
|
||||
Модуль jira: агент `jira`; свежесть — `jira_synced_at` из `GET /projects`, охват «все проекты»; действие «Обновить из
|
||||
Jira» — `POST /refresh {full: true, author: "viewer"}`; после пакета перечитываются снимки досок, списки задач, лента и
|
||||
открытая карточка; доска, показывающая снимок из файла, его не заменяет. Модуль wiki: агент `wiki`; свежесть —
|
||||
`wiki_synced_at`, `wiki_sync_error` из `GET /projects`, охват «проект»; данные для проекта есть, только если есть
|
||||
`wiki_root`; действие «Обновить вики» — `POST /wiki/sync {project, author: "viewer"}`; после пакета перечитываются дерево,
|
||||
страницы и поиск. Блок состояния MUST NOT загружать снимок доски или `GET /wiki/status`.
|
||||
|
||||
Страница модуля с действием обновления SHALL дублировать его вторичной кнопкой в шапке страницы; основной (primary)
|
||||
кнопки обновления в приложении нет.
|
||||
|
||||
#### Scenario: Обновление со списка задач
|
||||
- **WHEN** открыт `/app/jira/issues`, нажато «Обновить из Jira» в строке «Jira», пакет выполнен
|
||||
- **THEN** в хабе один пакет `refresh` автора `viewer`; список задач перезапрошен; строка «Jira» показывала «пакет #N
|
||||
выполняется», затем «пакет #N выполнен»; строка «Вики» не изменилась
|
||||
|
||||
#### Scenario: Один прогон на модуль
|
||||
- **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, выполнен переход между разделами и нажата «Обновить из
|
||||
Jira» в шапке доски
|
||||
- **THEN** кнопки «Обновить из Jira» недоступны, в хабе один пакет, его опрашивает один опрос
|
||||
|
||||
#### Scenario: Опрос получил 404
|
||||
- **WHEN** опрос пакета вики получил 404
|
||||
- **THEN** опрос остановлен, строка «Вики» показывает «пакета нет в хабе», «Обновить вики» снова доступна
|
||||
|
||||
#### Scenario: Автообновление хаба
|
||||
- **WHEN** хаб поставил пакет «Автообновление» (автор `hub`, модуль `jira`), приложение открыто на странице «Вики»
|
||||
- **THEN** не позже чем через 60 с строка «Jira» показывает «пакет #N выполняется», по завершении — новое «данные на …»
|
||||
|
||||
#### Scenario: Пакет другого проекта
|
||||
- **WHEN** идёт пакет `wiki_sync` проекта `STOCKS_DRRK`, текущий проект — `AWPCA`
|
||||
- **THEN** строка «Вики» его не показывает, «Обновить вики» доступна
|
||||
|
||||
#### Scenario: Пакет с другими командами
|
||||
- **WHEN** незавершённый пакет модуля jira автора `viewer` состоит из команд `ensure_fields`
|
||||
- **THEN** строка «Jira» показывает «пакет #N выполняется», кнопка «Обновить из Jira» доступна; её нажатие ставит пакет
|
||||
`refresh`, и строка показывает его
|
||||
|
||||
#### Scenario: Агент офлайн
|
||||
- **WHEN** агент `wiki` офлайн и нажато «Обновить вики»
|
||||
- **THEN** пакет поставлен; строка «Вики» — точка с текстом «агент офлайн» и событие «агент офлайн — пакет выполнится,
|
||||
когда он выйдет на связь; пакет #N в очереди…»
|
||||
|
||||
#### Scenario: Синхронизация уже идёт
|
||||
- **WHEN** нажато «Обновить вики», хаб ответил 409
|
||||
- **THEN** строка «Вики» показывает идущий пакет, сообщения об ошибке нет
|
||||
|
||||
#### Scenario: Проект без корня вики
|
||||
- **WHEN** текущий проект `sfm_kedr` без корня вики
|
||||
- **THEN** в блоке состояния только строка «Jira»; на свёрнутой панели — один значок состояния
|
||||
|
||||
#### Scenario: Агенту нужно обновление
|
||||
- **WHEN** `GET /agents` отдаёт агента `jira` с `warning`
|
||||
- **THEN** строка «Jira» — точка с текстом «обновите агента», текст `warning` в подсказке
|
||||
|
||||
#### Scenario: Смена проекта во время опроса
|
||||
- **WHEN** строка «Вики» показывает пакет `wiki_sync` проекта `AWPCA`, текущий проект сменён на `STOCKS_DRRK`, и в тот же
|
||||
момент пришёл ответ опроса этого пакета
|
||||
- **THEN** строка «Вики» без события пакета; опроса этого пакета больше нет
|
||||
Loading…
x
Reference in New Issue
Block a user