1041 lines
121 KiB
Markdown
1041 lines
121 KiB
Markdown
# web-app Specification
|
||
|
||
## Purpose
|
||
Веб-приложение хаба (`web/`, React + TypeScript + Vite) под префиксом `/app/`: вход по клиентскому токену, доска связей (перенос `links.html`) и страницы задач со служебными заметками; сборка в `src/sdlc_hub/web/app/`.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Вход по токену
|
||
Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в `localStorage` под ключом
|
||
`sdlc-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса.
|
||
Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с
|
||
подсказкой `uv run sdlc-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода.
|
||
Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен»
|
||
SHALL очищать токен и показывать экран входа. Ответ 401 SHALL выводить на экран входа, только если запрос ушёл с текущим токеном: запоздавший 401 на
|
||
запрос с прежним токеном после входа с новым не учитывается. Если проверка токена получила ответ 4xx, кроме 401,
|
||
экран входа SHALL показывать текст ошибки хаба, а не «хаб не отвечает».
|
||
|
||
Ключи `localStorage` приложения с префиксом `sdlc-hub-` (`sdlc-hub-token`, `sdlc-hub-sidebar`, `sdlc-hub-legend`) SHALL
|
||
при первом чтении, если ключа нет, брать значение одноимённого ключа с префиксом `jira-hub-`, записывать его под новым
|
||
ключом и удалять прежний.
|
||
|
||
#### Scenario: Первый вход
|
||
- **WHEN** приложение открыто впервые, токена нет
|
||
- **THEN** показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён
|
||
|
||
#### Scenario: Устаревший токен
|
||
- **WHEN** сохранённый токен не подходит и запрос ответил 401
|
||
- **THEN** показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны
|
||
|
||
#### Scenario: Токен от прежней доски
|
||
- **WHEN** в `localStorage` нет `sdlc-hub-token`, но есть `jira-hub-token` (сохранён до переименования хаба)
|
||
- **THEN** приложение входит с ним без запроса токена; значение перенесено в `sdlc-hub-token`, `jira-hub-token` удалён
|
||
|
||
#### Scenario: Запоздавший 401 от прежнего токена
|
||
- **WHEN** запрос ушёл с прежним токеном, пользователь вошёл с новым, и после этого запрос ответил 401
|
||
- **THEN** приложение остаётся на странице, новый токен сохранён
|
||
|
||
#### Scenario: Хаб отверг проверку токена
|
||
- **WHEN** проверка `GET /settings` ответила 400 с `detail`
|
||
- **THEN** экран входа показывает текст `detail`, а не «хаб не отвечает»
|
||
|
||
### Requirement: Оболочка и адреса
|
||
Приложение SHALL открываться под префиксом `/app/`. Главная страница — «Обзор» ядра (`/app/?project=<метка>`,
|
||
требование «Обзор проекта»). Страницы модулей SHALL лежать под префиксом имени модуля: модуль jira — `/app/jira/links`
|
||
(«Связи» — доска связей), `/app/jira/backlog` («Бэклог»), `/app/jira/sprint` («Спринт»), `/app/jira/kanban` («Kanban»;
|
||
три последних — требование «Спринты и бэклог»), `/app/jira/issues` (список задач), `/app/jira/feed` (лента изменений, требование «Лента изменений») и
|
||
`/app/jira/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); модуль wiki —
|
||
`/app/wiki` (требование «Вики проекта»). Прежние адреса `/app/board` и `/app/jira/board` (на `/app/jira/links`),
|
||
`/app/issues`, `/app/issues/{key}`, `/app/feed` SHALL перенаправлять на новые с сохранением строки запроса, без новой записи истории. Неизвестный путь — страница «не
|
||
найдено» со ссылками на «Обзор» и разделы модулей.
|
||
|
||
Боковая панель SHALL быть закреплена у левого края окна на всю его высоту и MUST NOT прокручиваться вместе со
|
||
страницей; если её содержимое не помещается по высоте, прокручивается сама панель. Сверху вниз панель SHALL содержать:
|
||
название «SDLC Hub» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий отмечен, в
|
||
конце «+ Новый проект…»; требование «Проект в приложении»); пункт «Обзор»; разделы модулей группами — заголовок группы
|
||
— название модуля («Jira», «Вики»), под ним ссылки разделов модуля со значками (текущий отмечен
|
||
`aria-current="page"`); внизу — блок состояния модулей (требование «Состояние и обновление модулей»), переключатель
|
||
темы (требование «Тема») и меню с действием «Сменить токен». Свёрнутая панель SHALL быть узкой полосой с теми же
|
||
действиями значками: «Развернуть панель», метка текущего проекта (полностью — в подсказке), «Обзор», разделы
|
||
(группы разделены линией), по значку состояния на модуль, тема, меню; названия — в подсказках и `aria-label`. Свёртка
|
||
SHALL храниться в `localStorage` под ключом `sdlc-hub-sidebar` (`collapsed`; без записи — развёрнута).
|
||
|
||
На экранах уже 640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», меткой текущего
|
||
проекта и сводной точкой состояния модулей (ошибка, если она есть у какого-нибудь модуля; иначе — пакет в работе;
|
||
иначе — все агенты на связи или нет), которая открывает панель. «Меню» открывает развёрнутую панель поверх страницы
|
||
модальным окном: фокус переходит в панель и остаётся в ней, страница под ней недоступна; панель закрывается выбором
|
||
раздела, клавишей Escape (если не открыто вложенное меню — тогда Escape закрывает только его) или щелчком мимо неё; фокус
|
||
возвращается на «Меню». Горизонтальной прокрутки страницы на любой ширине нет.
|
||
|
||
Заголовок вкладки браузера SHALL быть «<название страницы> · <метка проекта> · SDLC Hub» (без проекта — без средней
|
||
части); название страницы даёт оболочка («Обзор») или модуль. Неожиданная ошибка отрисовки страницы SHALL показываться
|
||
сообщением с кнопкой «Перезагрузить», не пустым экраном.
|
||
|
||
#### Scenario: Прежний адрес доски связей
|
||
- **WHEN** открыт `/app/jira/board?project=AWPCA`
|
||
- **THEN** адрес — `/app/jira/links?project=AWPCA`, показана доска связей, в меню отмечен пункт «Связи», заголовок
|
||
вкладки — «Связи · AWPCA · SDLC Hub»
|
||
|
||
#### Scenario: Переход между разделами
|
||
- **WHEN** на доске в боковой панели выбран раздел «Задачи» группы «Jira»
|
||
- **THEN** адрес — `/app/jira/issues` с текущим проектом, показан список задач; «назад» возвращает на доску
|
||
|
||
#### Scenario: Раздел «Лента»
|
||
- **WHEN** на доске в боковой панели выбран раздел «Лента»
|
||
- **THEN** адрес — `/app/jira/feed` с текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели
|
||
|
||
#### Scenario: Сменить токен
|
||
- **WHEN** в меню боковой панели выбрано «Сменить токен»
|
||
- **THEN** токен очищен, показан экран входа
|
||
|
||
#### Scenario: Панель на месте при прокрутке
|
||
- **WHEN** доска прокручена до самого низа
|
||
- **THEN** боковая панель видна целиком на прежнем месте: разделы и блок состояния модулей доступны без прокрутки вверх
|
||
|
||
#### Scenario: Свёрнутая панель
|
||
- **WHEN** нажата «Свернуть панель», страница перезагружена
|
||
- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; видны два значка
|
||
состояния — Jira и Вики; «Развернуть панель» возвращает полную панель
|
||
|
||
#### Scenario: Узкий экран
|
||
- **WHEN** ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи»
|
||
- **THEN** панель открылась поверх страницы, фокус в панели; после выбора панель закрыта, фокус на «Меню», показан список
|
||
задач; горизонтальной прокрутки нет
|
||
|
||
#### Scenario: Прежний адрес
|
||
- **WHEN** открыт `/app/issues/AURORA-1536?project=AWPCA`
|
||
- **THEN** адрес заменён на `/app/jira/issues/AURORA-1536?project=AWPCA`, карточка открыта поверх списка задач
|
||
|
||
#### Scenario: Заголовок вкладки
|
||
- **WHEN** открыта страница «Вики» проекта `AWPCA`
|
||
- **THEN** заголовок вкладки — «Вики · AWPCA · SDLC Hub»
|
||
|
||
### Requirement: Ошибки хаба
|
||
Приложение SHALL различать ошибки запросов к хабу: 401 — экран входа; хаб не отвечает (сеть, 5xx) — сообщение
|
||
«хаб не отвечает» с возможностью повторить, ранее загруженные данные остаются на экране; прочие 4xx — текст `detail`
|
||
ответа рядом с действием, которое его вызвало.
|
||
|
||
#### Scenario: Хаб остановлен
|
||
- **WHEN** доска показана, хаб остановлен, нажато «Перечитать»
|
||
- **THEN** на доске прежние данные и сообщение «хаб не отвечает — показаны прежние данные»
|
||
|
||
### 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 содержать заголовок «Доска связей», счётчики (задачи из всех, соседи по
|
||
связям, связи из всех, скрытые без связей), пометку источника, если на доске снимок из файла (с действием вернуться к
|
||
данным хаба), событие доски, кнопку «Проблемы» со счётчиком и меню с действиями «Перечитать», «Загрузить файл…»,
|
||
«Экспорт JSON», «Таблица связей», «Привязка к сетке» и «Сбросить раскладку» (с подтверждением). Под шапкой — панель
|
||
управления: мультиселекты «Спринты» и «Исполнители», переключатели группы «Показать» («Соседи по связям», «Только
|
||
передачи», «Скрыть без связей») и группы «Режим» («Критический путь», «По датам»), каждый с `aria-pressed` и прежней
|
||
подсказкой. Под панелью — легенда, которая сворачивается кнопкой «Легенда»; свёрнутая показывает число скрытых видов
|
||
работ и типов связей; состояние свёртки хранится в `localStorage` под ключом `sdlc-hub-legend`. «Обновить из Jira»,
|
||
тема и «Сменить токен» — в боковой панели приложения, на доске их нет.
|
||
|
||
У карточки задачи (не у заглушки) 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 восстанавливать прежнюю
|
||
прокрутку; после перезагрузки страницы доска открывается сверху.
|
||
|
||
Дорожки SHALL разделяться линией; фон развёрнутых дорожек один, без чередования, чтобы подсветка колонок «по датам»
|
||
(прошедшие даты, сегодня) была видна на всех дорожках; свёрнутая дорожка выделяется фоном. У карточки задачи, статус
|
||
Jira которой не входит в `closed_statuses`, с плановой датой окончания раньше сегодняшней дата «до …» 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 — в полную яркость; критический путь и ящик «Проблемы» те же, что без приглушения
|
||
|
||
#### Scenario: Свёрнутая легенда
|
||
- **WHEN** в легенде скрыт вид работы «тестирование», легенда свёрнута, страница перезагружена
|
||
- **THEN** легенда свёрнута, рядом с кнопкой «Легенда» — «скрыто: 1»; карточек вида «тестирование» на доске нет
|
||
|
||
#### Scenario: Просрочка на доске
|
||
- **WHEN** сегодня 22.10, у AURORA-1620 статус «В работе» и срок 20.10, у AURORA-1621 «Готово» и срок 20.10, включено
|
||
«По датам»
|
||
- **THEN** дата AURORA-1620 — цветом ошибки с пометкой «просрочено», у AURORA-1621 — обычная; подсветка колонки 20.10
|
||
видна на всех дорожках, дорожки одного фона
|
||
|
||
### Requirement: Источник данных доски
|
||
Доска SHALL хранить явный источник данных — хаб или файл. Пока показан снимок из файла (CSV или JSON-снимок), ответы
|
||
хаба SHALL его не заменять; «Перечитать» SHALL возвращать источник «хаб» и загружать `GET /board` заново без обращения к
|
||
Jira. Импорт JSON в режиме хаба SHALL применять раскладку, дорожки и фильтры и перечитывать хаб. Шапка доски SHALL
|
||
показывать источник данных (пометка «из файла» с именем файла; данные хаба пометки не имеют) и одно текущее событие
|
||
доски (загрузка; хаб не отвечает — прежние данные, а при снимке из файла — «на доске снимок из файла»; ошибка разбора
|
||
файла — «файл не разобран: <причина>»; раскладка и фильтры из JSON применены; ошибка); новое событие заменяет
|
||
предыдущее целиком. События пакета обновления из Jira показывает боковая панель приложения (требование «Обновление из
|
||
Jira»), не шапка доски.
|
||
|
||
#### Scenario: Поздний ответ хаба не затирает файл
|
||
- **WHEN** запрос `GET /board` в пути, и в это время загружен CSV
|
||
- **THEN** после ответа хаба на доске по-прежнему данные CSV с пометкой «из файла»
|
||
|
||
#### Scenario: Пакет завершился при снимке из файла
|
||
- **WHEN** на доске снимок из CSV и завершился пакет обновления из Jira
|
||
- **THEN** на доске по-прежнему данные CSV с пометкой «из файла»; после «Перечитать» — данные хаба
|
||
|
||
#### Scenario: Хаб недоступен при снимке из файла
|
||
- **WHEN** хаб не отвечает и загружен JSON-снимок доски
|
||
- **THEN** шапка показывает «хаб не отвечает — на доске снимок из файла», а не «показаны прежние данные»
|
||
|
||
#### Scenario: Нет второго полного прогона
|
||
- **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, доска перерисована (фильтр, «Перечитать»)
|
||
- **THEN** кнопка «Обновить из Jira» в боковой панели остаётся недоступной, в хабе один пакет, его опрашивает один опрос
|
||
|
||
#### Scenario: Пакет пропал
|
||
- **WHEN** опрос пакета получил 404, на доске данные хаба
|
||
- **THEN** доска показывает прежние данные без своего события об ошибке; сообщение о пропавшем пакете — в боковой панели
|
||
|
||
#### Scenario: Пакет после «Перечитать»
|
||
- **WHEN** пакет `viewer` поставлен с другой вкладки и не завершён, на доске нажато «Перечитать»
|
||
- **THEN** боковая панель показывает «пакет #N выполняется», по завершении доска перечитывает данные
|
||
|
||
#### Scenario: «Перечитать» во время пакета
|
||
- **WHEN** пакет `viewer` выполняется и на доске нажато «Перечитать»
|
||
- **THEN** боковая панель по-прежнему показывает «пакет #N выполняется», шапка доски — без события пакета
|
||
|
||
#### Scenario: Чужой пакет не подхватывается
|
||
- **WHEN** незавершённый пакет автора `viewer` состоит из команд `ensure_fields`, открыта доска
|
||
- **THEN** доска и боковая панель его не опрашивают, кнопка «Обновить из Jira» доступна
|
||
|
||
### 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: Сборка и разработка
|
||
Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и
|
||
`package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в
|
||
`src/sdlc_hub/web/app/`; каталог сборки не хранится в git. Шрифты (Mulish — текст интерфейса, IBM Plex Mono — ключи
|
||
задач и исходники), иконки и прочие ресурсы страницы SHALL входить в сборку: приложение не обращается к внешним
|
||
адресам. `make check` SHALL запускать `uv run pytest`, проверку типов и линтер фронта и тесты фронта; зелёным считается
|
||
только весь набор. `npm run dev` SHALL поднимать сервер разработки с прокси маршрутов API на хаб из `SDLC_HUB_URL` (по
|
||
умолчанию тестовый `http://127.0.0.1:8791`).
|
||
|
||
#### Scenario: Сборка с нуля
|
||
- **WHEN** в чистом checkout выполнен `make web`
|
||
- **THEN** в `src/sdlc_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки
|
||
|
||
#### Scenario: Без интернета
|
||
- **WHEN** приложение открыто на машине без доступа в интернет
|
||
- **THEN** страница не запрашивает ресурсов вне хаба, текст набран Mulish, ключи задач — IBM Plex Mono
|
||
|
||
### 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: Дизайн-система
|
||
Приложение SHALL следовать дизайн-системе AURA (handoff AURA-xlsx-tool 29.09.2026, Figma «Универсалки») в светлой и
|
||
тёмной темах. Токены — цвета по семантическим ролям для обеих тем, типографика, отступы с шагом 4 px, радиусы, высоты
|
||
контролов, фокус, движение — SHALL быть определены в `web/src/styles/tokens.css` (вместе с токенами приложения: палитра
|
||
видов работ, приоритеты, стрелки и дорожки доски); прочие файлы стилей MUST NOT содержать литералов цвета и SHALL
|
||
брать цвета из токенов. Экраны SHALL собираться из компонентов `web/src/ui/` (кнопки, кнопка-иконка, сегментный
|
||
переключатель, переключатель темы, поле с подписью, поле ввода и поиска, выпадающий список, флажок, бейдж, снимаемая
|
||
плашка, сообщение, панель, шапка страницы, окно, меню). Геометрия: контролы 32 px (крупные 40 px) с радиусом 10 px,
|
||
кнопки — «таблетки», панели — радиус 16 px, окна — радиус 20 px, флажок 16×16 px; текст Regular 14/20, подписи
|
||
Semibold 14/20, заголовок страницы Bold 20/28. Страницы, кроме доски, SHALL занимать контейнер до 1440 px по центру с
|
||
боковым отступом 24 px (16 px на экранах уже 640 px); доска — всю ширину окна с отступом 24 px. Иконки — SVG из
|
||
сборки, не символы текста. Каждый интерактивный элемент SHALL иметь видимое кольцо фокуса; цвет MUST NOT быть
|
||
единственным признаком состояния.
|
||
|
||
#### Scenario: Цвет вне токенов
|
||
- **WHEN** в любом `*.css` в `web/src`, кроме `styles/tokens.css`, появился литерал цвета
|
||
- **THEN** тесты фронта падают с именем файла и литералом
|
||
|
||
#### Scenario: Тёмная тема
|
||
- **WHEN** выбрана тёмная тема
|
||
- **THEN** канва `#181818`, поверхности `#1f1f1f`, основной текст `#f2f4f2`, основное действие `#69a93d` на всех
|
||
страницах, в окнах и меню
|
||
|
||
### Requirement: Тема
|
||
Переключатель темы SHALL быть в боковой панели приложения: два положения — светлая и тёмная. Выбор SHALL храниться в
|
||
`localStorage` под ключом `graph-theme` (`light` / `dark`), как у прежней доски; без сохранённого выбора или со
|
||
значением `""` тема SHALL следовать настройке системы, и переключатель показывает её. Тема применяется до первой
|
||
отрисовки, без мигания.
|
||
|
||
#### Scenario: Выбор темы сохраняется
|
||
- **WHEN** в системе светлая тема, переключатель переведён в «тёмная», страница перезагружена
|
||
- **THEN** приложение в тёмной теме, в `localStorage` `graph-theme` = `dark`
|
||
|
||
### 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 показывать в основной колонке после комментариев блок «История (N)» — события `changes` из
|
||
`GET /issues/{key}` (все виды, новые сверху) в том же виде, что в ленте, всегда с датой. Видны 10 последних событий,
|
||
остальные — по кнопке «Показать все (N)»; без событий — подсказка «изменений нет»; ответ хаба без поля `changes` —
|
||
блока нет.
|
||
|
||
#### Scenario: Карточка с историей
|
||
- **WHEN** открыта карточка задачи с 12 событиями журнала
|
||
- **THEN** блок «История (12)» показывает 10 последних событий и кнопку «Показать все (12)»; после нажатия — все 12
|
||
|
||
### 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 иметь страницу «Вики» (`/app/wiki?project=<метка>`, раздел группы «Вики» боковой панели; текущий
|
||
проект — как у остальных страниц, «Проект в приложении»). Шапка страницы: заголовок «Вики», подзаголовок «<название
|
||
корня> · <N> страниц · обновлено ДД.ММ ЧЧ:ММ» и действие «Обновить вики» (вторичная кнопка; требование «Состояние и
|
||
обновление модулей»). Страница SHALL показывать:
|
||
- дерево страниц проекта (`GET /wiki/tree`) слева, свёрнутое до первого уровня; узел раскрывается по клику; узел
|
||
выбранной страницы и его предки раскрыты;
|
||
- выбранную страницу справа (`GET /wiki/pages/{code}`): заголовок, дата синхронизации, ссылка «открыть в Eva»,
|
||
Markdown с таблицами, без сырого HTML; без `page` в адресе — корневая страница; ссылки `page:<код>` открывают страницу
|
||
вики (код в адресе `&page=<код>`), ссылки `attachment:<id>` скачивают вложение (запрос, опрос состояния, сохранение
|
||
файла браузером), состояние `failed` — текст ошибки рядом со ссылкой;
|
||
- на экранах уже 640 px выбранная страница — над деревом;
|
||
- поиск над деревом (`GET /wiki/search`): список результатов с фрагментами вместо дерева, пока строка поиска не пуста;
|
||
после очистки поиска раскрытие дерева прежнее;
|
||
- проекта без корня — форму «Корень вики (DOC-… или ссылка)» с кнопкой «Привязать» (`PATCH /projects/{label}`); 422 —
|
||
текст ошибки у поля;
|
||
- корень есть, синхронизаций нет — «вики ещё не синхронизирована» и действие «Обновить вики»;
|
||
- последняя синхронизация с ошибкой — плашку с текстом ошибки над страницей.
|
||
|
||
#### Scenario: Открыть страницу по ссылке
|
||
- **WHEN** на странице DOC-3 нажата ссылка `page:DOC-2`
|
||
- **THEN** адрес `/app/wiki?project=AWPCA&page=DOC-2`, справа DOC-2, в дереве DOC-2 выделен
|
||
|
||
#### Scenario: Проект без корня
|
||
- **WHEN** открыта `/app/wiki?project=sfm_kedr`, корня нет; введено `DOC-001234`, нажато «Привязать»
|
||
- **THEN** `PATCH /projects/sfm_kedr {wiki_root: "DOC-001234"}`; строка «Вики» блока состояния показывает ход пакета
|
||
синхронизации
|
||
|
||
#### Scenario: Поиск
|
||
- **WHEN** в строке поиска «согласов»
|
||
- **THEN** вместо дерева — результаты `GET /wiki/search?project=AWPCA&q=согласов`; очистка строки возвращает дерево
|
||
|
||
#### Scenario: Без выбранной страницы
|
||
- **WHEN** открыта `/app/wiki?project=AWPCA` без `page`
|
||
- **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** строка «Вики» без события пакета; опроса этого пакета больше нет
|
||
|
||
### Requirement: Обзор проекта
|
||
Страница ядра «Обзор» (`/app/?project=<метка>`, пункт «Обзор» боковой панели) SHALL показывать шапку «Обзор» с
|
||
названием проекта и под ней карточку на каждый модуль, у которого есть данные для проекта: название модуля, агент (с
|
||
версией из `GET /agents`), свежесть с охватом, текущее событие пакета, вторичная кнопка обновления и сводка, которую
|
||
даёт модуль. Сводка jira: число своих задач проекта, из них открытых и просроченных (по снимку доски проекта), ссылки
|
||
«Доска связей», «Задачи», «Лента». Сводка wiki: число страниц, поле поиска по вики (Enter открывает «Вики» с этим
|
||
поиском), ссылка «Вики». Модуль без данных для проекта SHALL показывать карточку с объяснением и действием модуля (у
|
||
вики — «Привязать корень вики» ведёт на страницу «Вики»). Под карточками — «Последние пакеты проекта»: до 10 из
|
||
`GET /batches?project=<метка>`: номер, модуль, название, автор, статус, время создания.
|
||
|
||
#### Scenario: Главная
|
||
- **WHEN** открыт `/app/` без проекта, последний выбранный — `AWPCA`
|
||
- **THEN** адрес `/app/?project=AWPCA`, две карточки — «Jira» и «Вики», список последних пакетов
|
||
|
||
#### Scenario: Поиск по вики с обзора
|
||
- **WHEN** на «Обзоре» `AWPCA` в поле поиска вики введено «согласов» и нажат Enter
|
||
- **THEN** открыта «Вики» с результатами поиска «согласов»
|
||
|
||
#### Scenario: Проект без вики
|
||
- **WHEN** «Обзор» проекта `sfm_kedr` без корня вики
|
||
- **THEN** карточка «Вики» с объяснением и ссылкой «Привязать корень вики»
|
||
|
||
### Requirement: Граница модулей в вебе
|
||
Ядро веба (`web/src/{shell,ui,api,projects,auth,styles}`) MUST NOT импортировать модули, кроме списка модулей
|
||
`modules/index.ts`, и MUST NOT содержать типов, справочников и терминов модулей (типы задач Jira, закрытые статусы,
|
||
словари видов работ и связей, «статус по факту»). Модуль MUST NOT импортировать другой модуль; общее (форматтеры дат
|
||
«ДД.ММ», «ДД.ММ ЧЧ:ММ», «ДД.ММ.ГГГГ» для дат не текущего года, скачивание файла) — в ядре. Имена модулей и адреса
|
||
разделов (`to`) SHALL быть уникальны. Имя класса CSS, объявленное в стилях модуля, MUST NOT объявляться в стилях
|
||
другого модуля или ядра. Даты в списке задач и карточке SHALL выводиться форматтерами ядра, не строкой ISO.
|
||
|
||
#### Scenario: Пересечение классов
|
||
- **WHEN** в `modules/jira/**.css` и `modules/wiki/**.css` объявлен класс `.wiki`
|
||
- **THEN** тесты фронта падают с именем класса и файлами
|
||
|
||
#### Scenario: Импорт чужого модуля
|
||
- **WHEN** файл в `modules/wiki/` импортирует из `modules/jira/`
|
||
- **THEN** тесты фронта падают с именем файла
|
||
|
||
#### Scenario: Дубль адреса раздела
|
||
- **WHEN** два модуля объявляют раздел с одинаковым `to`
|
||
- **THEN** тесты фронта падают с адресом и модулями
|
||
|
||
### Requirement: Спринты и бэклог
|
||
Модуль jira SHALL показывать для проекта с доской страницы по образцу Jira — только для просмотра: на них MUST NOT быть
|
||
действий, меняющих Jira (перетаскивания, кнопок спринтов, смены статуса или ранга). Пункты меню группы «Jira» —
|
||
«Связи», «Задачи», «Лента» и по типу доски текущего проекта (`board_type` из `GET /projects`): `scrum` — «Бэклог» и
|
||
«Спринт»; `kanban` — «Kanban»; доски нет или тип неизвестен — новых пунктов нет, а прямой переход на их адрес
|
||
показывает «У проекта нет scrum-доски» («…kanban-доски» для «Kanban») со ссылкой на «Связи». Данные — `GET
|
||
/agile?project=<текущий>`; если доска есть, но её данных от агента нет (`board = null` или `board.fetched_at = null`) —
|
||
«Нужен агент v15 и обновление из Jira» с кнопкой «Обновить» блока состояния модуля; данные доски есть (`fetched_at` задан), а колонок нет (`columns = null`) — «Не удалось
|
||
прочитать настройки доски из Jira» и та же кнопка. Пока проекты не загружены, условные пункты меню скрыты; смена проекта
|
||
на странице, которой у нового проекта нет, оставляет адрес и показывает сообщение «У проекта нет …-доски». Даты
|
||
спринтов (ISO-8601 Jira) показываются «ДД.ММ» в часовом поясе браузера; «осталось N дн.» — от сегодня до конца спринта.
|
||
|
||
Строка задачи и карточка задачи на этих страницах: ключ, значок типа, тема, исполнитель (или «—»), оценка (число или
|
||
«—»); выполненная — приглушена. Щелчок открывает карточку задачи (окно поверх страницы, как в списке задач). Оценки и
|
||
их суммы SHALL показываться числом не более чем с двумя знаками после запятой без хвостовых нулей (`0.1 + 0.2` —
|
||
«0.3», `3` — «3»).
|
||
|
||
**Значок типа** SHALL различать по `issue_type` без учёта регистра: эпик («Эпик», «Epic», «Фича»), история
|
||
(«История», «Story»), задача («Задача», «Task»), ошибка («Ошибка», «Bug»), подзадача («Подзадача», «Sub-task»,
|
||
«Subtask»); прочие и пустой тип — нейтральный значок. Значок — цветной квадрат с символом цвета `--color-text-on-primary`, контрастным в обеих темах, у каждого вида свои
|
||
цвет и символ (эпик — молния, история — закладка, задача — галочка, ошибка — круг, подзадача — два квадрата), в обеих
|
||
темах; название типа — в `aria-label` и подсказке.
|
||
|
||
**Панель фильтров** — над содержимым «Бэклога», «Спринта» и «Kanban»: поиск «Ключ или тема» (подстрока без учёта
|
||
регистра в ключе или теме), выбор «Исполнитель» («все», «Без исполнителя», исполнители задач страницы по алфавиту),
|
||
переключатель «Скрыть выполненные» (только «Бэклог»; на «Спринте» и «Kanban» выполненное скрывают колонкой, сохранённое
|
||
значение там не применяется); на «Бэклоге» — кнопка «Эпики» (показать или скрыть панель эпиков); на «Спринте» — выбор
|
||
спринтов; на «Спринте» и «Kanban» — меню «Колонки». Кнопка «Сбросить фильтры» SHALL быть в панели всегда и снимать все
|
||
фильтры (поиск, исполнитель, эпик, «Скрыть выполненные»); пока ни один не задан, она неактивна — панель не сдвигается,
|
||
когда фильтр появляется или снимается. Фильтры действуют вместе
|
||
(И). Сохранённое значение исполнителя или эпика, которого нет в данных, SHALL оставаться выбранным и видимым в списке.
|
||
|
||
**Запоминание:** свёрнутые секции, выбранный эпик, исполнитель, «Скрыть выполненные», видимость панели эпиков и скрытые
|
||
колонки SHALL храниться в `localStorage` под ключом `sdlc-hub-agile` — JSON по метке проекта и странице (`backlog`,
|
||
`sprint`, `kanban`); у разных страниц и проектов свои значения. Поиск и выбор спринтов не запоминаются. Нечитаемое или
|
||
закрытое хранилище — значения по умолчанию, страница работает.
|
||
|
||
**«Бэклог»** (`/app/jira/backlog`), сверху вниз: секции активных спринтов, будущих спринтов, «Бэклог», затем
|
||
свёрнутый список «Прошлые спринты (N)». Заголовок секции спринта: имя, состояние, даты «ДД.ММ–ДД.ММ», цель (если есть),
|
||
«N задач», «S SP (выполнено D)» и «без оценки: U», если `U > 0` — итоги спринта, фильтры их не меняют; если фильтры
|
||
скрыли часть задач секции — ещё «показано K». Задачи секции — по рангу. Секция без задач показывает «Нет задач»; все
|
||
задачи скрыты фильтрами — «Нет задач по фильтрам». Любую секцию (спринта и «Бэклог») SHALL можно свернуть и развернуть
|
||
щелчком по заголовку (кнопка с `aria-expanded`); по умолчанию развёрнуты все. Прошлый спринт в списке — строка «имя ·
|
||
закрыт ДД.ММ · выполнено X из N · S SP»; раскрытие показывает его задачи с пометкой «не выполнено» у невыполненных.
|
||
|
||
Эпики (задачи вида «эпик») MUST NOT показываться строками в секциях «Бэклога». Панель «Эпики» — слева от секций:
|
||
пункт «Все задачи», затем эпики — каждая задача-эпик из данных страницы по рангу, затем эпики, на которые ссылается
|
||
`epic_key` задач, но строки которых в данных нет, по ключу; в конце — «Без эпика». Пункт эпика: тема (или ключ, если
|
||
темы нет), ключ, число задач эпика в секциях активных и будущих спринтов и «Бэклога». Выбор пункта (кнопка с
|
||
`aria-pressed`) фильтрует все секции, включая прошлые спринты; повторный выбор выбранного эпика возвращает «Все
|
||
задачи». При «Скрыть выполненные» выполненные эпики в панели не показываются (кроме выбранного). На узком экране
|
||
(контейнер страницы уже 900 px) панель SHALL становиться выбором «Эпик» в панели фильтров.
|
||
|
||
«Скрыть выполненные» на «Бэклоге» скрывает выполненные задачи в секциях активных и будущих спринтов и «Бэклога»;
|
||
прошлые спринты показываются полностью.
|
||
|
||
**«Спринт»** (`/app/jira/sprint`): выбранные спринты доски — по умолчанию все активные. Выбор спринтов — список с
|
||
отметками: активные, затем будущие (по дате начала, без даты — в конце); выбор хранится в адресе
|
||
`?sprint=<id>[,<id>…]` (переход без новой записи истории); идентификаторы, которых нет среди активных и будущих,
|
||
отбрасываются, если не осталось ни одного — все активные. Над доской — заголовок каждого выбранного спринта (имя,
|
||
даты, «осталось N дн.» или «срок прошёл» — только у активного, цель, суммы оценок), под ними колонки доски Jira с числом
|
||
задач; задачи доски — задачи выбранных спринтов, карточки в колонке — по рангу; колонка «Без колонки» — для статусов вне
|
||
настроек доски, только если в ней есть задачи. Нет активного спринта и ничего не выбрано — «Нет активного спринта» и
|
||
ссылка на «Бэклог»; если есть будущие спринты, выбор спринтов при этом доступен.
|
||
|
||
**«Kanban»** (`/app/jira/kanban`): колонки доски из `board_issues`, карточки по рангу, у последней колонки подпись
|
||
«за 14 дней»; если `kanban_backlog = true` — над колонками свёрнутый список «Бэклог (N)» по рангу; поиск и исполнитель
|
||
действуют и на него.
|
||
|
||
**Колонки доски** («Спринт» и «Kanban»): заголовок колонки — `h3`. Меню «Колонки» — отметки по колонкам доски (и «Без
|
||
колонки», если она есть); снятая отметка скрывает колонку, последнюю видимую скрыть нельзя (отметка недоступна); если
|
||
скрыты колонки — на кнопке их число. Доска SHALL занимать высоту окна под заголовком и панелью фильтров (не меньше
|
||
360 px; на окне уже 640 px — не меньше 240 px), все колонки — во всю высоту доски; карточки, не помещающиеся по высоте, прокручиваются внутри своей колонки.
|
||
Колонки, не помещающиеся по ширине, SHALL прокручиваться по горизонтали внутри доски, не страницы, и полоса горизонтальной
|
||
прокрутки доски SHALL находиться в пределах окна при любом числе карточек. Заголовок вкладки — «Бэклог», «Спринт»,
|
||
«Kanban» по общему правилу оболочки.
|
||
|
||
#### Scenario: Меню scrum-проекта
|
||
- **WHEN** текущий проект `AWPCA`, `board_type = scrum`
|
||
- **THEN** в группе «Jira» пункты «Связи», «Бэклог», «Спринт», «Задачи», «Лента»; пункта «Kanban» нет
|
||
|
||
#### Scenario: Проект без доски
|
||
- **WHEN** текущий проект `STOCKS_DRRK` без доски, открыт `/app/jira/backlog?project=STOCKS_DRRK`
|
||
- **THEN** пунктов «Бэклог», «Спринт», «Kanban» в меню нет; страница — «У проекта нет scrum-доски» со ссылкой на «Связи»
|
||
|
||
#### Scenario: Бэклог только смотрит
|
||
- **WHEN** открыт «Бэклог» `AWPCA`
|
||
- **THEN** на странице нет перетаскиваемых элементов; кнопки и поля — только свёртка секций, панель фильтров, пункты
|
||
панели эпиков и открытие карточки задачи; ни одно из них не ставит команд
|
||
|
||
#### Scenario: Прошлый спринт
|
||
- **WHEN** спринт `AWPCA_Sprint_2_VGO` закрыт, в нём 73 задачи, 61 в последней колонке, оценки у 10 задач на 25 SP
|
||
- **THEN** в «Прошлые спринты» строка «AWPCA_Sprint_2_VGO · закрыт ДД.ММ · выполнено 61 из 73 · 25 SP»; раскрытие —
|
||
73 задачи, 12 с пометкой «не выполнено»
|
||
|
||
#### Scenario: Агент без досок
|
||
- **WHEN** `AWPCA` scrum, агент v14 (данных доски нет), открыт «Спринт»
|
||
- **THEN** «Нужен агент v15 и обновление из Jira» и кнопка «Обновить»
|
||
|
||
#### Scenario: Свёрнутый активный спринт
|
||
- **WHEN** на «Бэклоге» свёрнута секция активного спринта, страница перезагружена
|
||
- **THEN** секция свёрнута (видны заголовок и итоги, задач нет), остальные развёрнуты
|
||
|
||
#### Scenario: Фильтр по эпику
|
||
- **WHEN** в бэклоге 3 задачи эпика `AURORA-10` и 5 других, в активном спринте 2 задачи `AURORA-10`; в панели «Эпики»
|
||
выбран `AURORA-10`
|
||
- **THEN** в секции спринта 2 задачи и «показано 2», в «Бэклоге» 3; строки самого `AURORA-10` в секциях нет; у пункта
|
||
`AURORA-10` в панели — 5
|
||
|
||
#### Scenario: Скрыть выполненные
|
||
- **WHEN** в активном спринте 10 задач, 4 выполнены; включено «Скрыть выполненные»
|
||
- **THEN** в секции 6 задач, заголовок — «10 задач» и «показано 6»; «Прошлые спринты» показывают все свои задачи
|
||
|
||
#### Scenario: Исполнитель на доске спринта
|
||
- **WHEN** на «Спринте» выбран исполнитель `ivanov`
|
||
- **THEN** на доске только его карточки; числа в заголовках колонок — по видимым карточкам; на «Бэклоге» исполнитель не
|
||
выбран (свой фильтр у каждой страницы)
|
||
|
||
#### Scenario: Будущий спринт на доске
|
||
- **WHEN** у доски активный спринт 101 и будущий 102; в выборе спринтов отмечен и 102
|
||
- **THEN** адрес — `?sprint=101,102`; над доской два заголовка, «осталось N дн.» только у 101; на доске задачи обоих
|
||
|
||
#### Scenario: Сброс фильтров на месте
|
||
- **WHEN** открыт «Спринт» без фильтров, затем выбран исполнитель
|
||
- **THEN** «Сбросить фильтры» видна и неактивна до выбора, после — активна на том же месте; флажка «Скрыть выполненные»
|
||
на «Спринте» нет
|
||
|
||
#### Scenario: Скрытая колонка
|
||
- **WHEN** на «Спринте» в меню «Колонки» снята отметка «Выполнено», страница перезагружена
|
||
- **THEN** колонки «Выполнено» нет, на кнопке «Колонки» — 1; остальные колонки на месте
|
||
|
||
#### Scenario: Широкая доска
|
||
- **WHEN** окно 2560 × 1300 px, у доски 12 колонок, в одной из них 40 карточек
|
||
- **THEN** все колонки одной высоты и помещаются в окно по высоте; полоса горизонтальной прокрутки доски видна в окне;
|
||
прокрутка доски вправо до конца показывает «Без колонки» целиком; страница горизонтально не прокручивается
|