70 KiB
web-app Specification
Purpose
Веб-приложение хаба (web/, React + TypeScript + Vite) под префиксом /app/: вход по клиентскому токену, доска связей (перенос links.html) и страницы задач со служебными заметками; сборка в src/jira_hub/web/app/.
Requirements
Requirement: Вход по токену
Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в localStorage под ключом
jira-hub-token (и в памяти, если хранилище закрыто) и передавать в заголовке Authorization: Bearer каждого запроса.
Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с
подсказкой uv run jira-hub tokens, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода.
Введённый токен SHALL проверяться запросом GET /settings и сохраняться только при успехе. Действие «Сменить токен»
SHALL очищать токен и показывать экран входа. Ответ 401 SHALL выводить на экран входа, только если запрос ушёл с текущим токеном: запоздавший 401 на
запрос с прежним токеном после входа с новым не учитывается. Если проверка токена получила ответ 4xx, кроме 401,
экран входа SHALL показывать текст ошибки хаба, а не «хаб не отвечает».
Scenario: Первый вход
- WHEN приложение открыто впервые, токена нет
- THEN показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён
Scenario: Устаревший токен
- WHEN сохранённый токен не подходит и запрос ответил 401
- THEN показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны
Scenario: Токен от прежней доски
- WHEN в
localStorageестьjira-hub-token, сохранённый страницейlinks.html - THEN приложение входит с ним без запроса токена
Scenario: Запоздавший 401 от прежнего токена
- WHEN запрос ушёл с прежним токеном, пользователь вошёл с новым, и после этого запрос ответил 401
- THEN приложение остаётся на странице, новый токен сохранён
Scenario: Хаб отверг проверку токена
- WHEN проверка
GET /settingsответила 400 сdetail - THEN экран входа показывает текст
detail, а не «хаб не отвечает»
Requirement: Оболочка и адреса
Приложение SHALL открываться под префиксом /app/ и иметь страницы /app/board (доска связей), /app/issues (список
задач), /app/feed (лента изменений, требование «Лента изменений») и /app/issues/{key} (карточка задачи — окно поверх страницы, требование «Карточка задачи»); /app и /app/ SHALL вести на /app/board, неизвестный путь —
на страницу «не найдено» со ссылками на разделы. Боковая панель SHALL быть закреплена у левого края окна на всю его
высоту и MUST NOT прокручиваться вместе со страницей; страница занимает остальную ширину. Сверху вниз панель SHALL
содержать: название «Хаб Jira» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий
отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»); разделы «Доска связей», «Задачи» и «Лента» — вертикальным
списком ссылок со значками (текущий отмечен aria-current="page"); внизу — блок данных Jira с действием «Обновить из
Jira» (требование «Обновление из Jira»), переключатель темы (требование «Тема») и меню с действием «Сменить токен».
Свёрнутая панель SHALL быть узкой полосой с теми же действиями значками: «Развернуть панель», метка текущего проекта,
разделы, «Обновить из Jira» с точкой состояния агента, тема, меню; названия — в подсказках и aria-label. Свёртка
SHALL храниться в localStorage под ключом jira-hub-sidebar (collapsed; без записи — развёрнута). На экранах уже
640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», названием и кнопкой «Обновить из
Jira»; «Меню» открывает развёрнутую панель поверх страницы, панель закрывается выбором раздела, клавишей Escape или
щелчком мимо неё. Горизонтальной прокрутки страницы на любой ширине нет.
Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не пустым экраном.
Scenario: Переход между разделами
- WHEN на доске в боковой панели выбран раздел «Задачи»
- THEN адрес —
/app/issuesс текущим проектом, показан список задач; «назад» возвращает на доску
Scenario: Раздел «Лента»
- WHEN на доске в боковой панели выбран раздел «Лента»
- THEN адрес —
/app/feedс текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели
Scenario: Сменить токен
- WHEN в меню боковой панели выбрано «Сменить токен»
- THEN токен очищен, показан экран входа
Scenario: Панель на месте при прокрутке
- WHEN доска прокручена до самого низа
- THEN боковая панель видна целиком на прежнем месте: разделы и «Обновить из Jira» доступны без прокрутки вверх
Scenario: Свёрнутая панель
- WHEN нажата «Свернуть панель», страница перезагружена
- THEN панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; «Развернуть панель» возвращает полную панель
Scenario: Узкий экран
- WHEN ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи»
- THEN панель открылась поверх страницы, после выбора закрыта, показан список задач; горизонтальной прокрутки нет
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 под ключом jira-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 быть доступна, пока последняя
порция полная.
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, в таблице задачи в статусе «Готово»
Requirement: Карточка задачи
Адрес /app/issues/{key} SHALL открывать карточку задачи из GET /issues/{key} модальным окном поверх страницы, с
которой её открыли (список «Задачи» или доска); страница под окном SHALL оставаться смонтированной и сохранять фильтры,
загруженные строки, прокрутку и раскладку. Перезагрузка страницы с открытым окном SHALL сохранять страницу под окном. Открытый напрямую
адрес (новая вкладка, закладка) SHALL
показывать окно поверх списка задач без фильтров. Окно SHALL закрываться кнопкой ✕, клавишей Esc и щелчком по фону (нажатие и отпускание — на фоне; выделение текста,
отпущенное за краем окна, окно не закрывает) и
возвращать на страницу, с которой его открыли, даже после переходов по связям внутри окна; «назад» браузера после
закрытия MUST NOT открывать карточку снова. Переход по связанной задаче SHALL менять содержимое окна и адрес, «назад»
браузера — возвращать к прежней карточке. Пока окно открыто, фокус SHALL оставаться в окне, страница под ним не
прокручивается.
Содержимое окна:
- шапка: ключ, статус Jira, «Открыть в Jira», ✕, тема; строка
«исполнитель · срок · спринт · эпик» без пустых частей, срок —
planned_end, иначеduedate, с пометкой «просрочено», если он раньше сегодняшней даты и статус Jira не входит вclosed_statuses; эпик — темой (epic_summary), иначе ключом; - «Связи» по группам в порядке: «Зависит от» (входящие связи логических типов
блокирует началоиблокирует завершениеизlink_types, с меткой «начало» / «завершение»), «Блокирует» (исходящие тех же типов, с теми же метками), «Связана с» (типсвязанныев обе стороны), «Прочие» (тип Jira внеlink_types: имя типа и направление «→» / «←»); в строке — ключ (открывает связанную задачу в окне), тема и статус Jira, а если задачи нет в зеркале — ключ и «нет в зеркале»; пустые группы не показываются, без связей — «связей нет»; - «Описание» — текст, отрендеренный по требованию «Разметка Jira в карточке», с кнопкой «исходник» / «текст» в заголовке раздела; длиннее 8 строк — свёрнуто с кнопкой «развернуть» / «свернуть» в обоих режимах; без описания раздела нет;
- «Комментарии (N)» — новые сверху, автор, время, кнопка «исходник» / «текст» у каждого комментария, текст, отрендеренный по требованию «Разметка Jira в карточке»; кнопка «Скрыть» / «Показать» сворачивает раздел, выбор SHALL запоминаться в браузере для всех карточек; без комментариев — «комментариев нет»;
- «Служебные заметки» (требование «Служебные заметки»);
- «Прочие поля» — непустые из: тип, приоритет, автор, метки, плановое начало, обновлена в Jira, сосед по связям;
- «Команды моста (N)» — 10 последних команд по задаче (id, пакет, тип, статус, время завершения, ошибка), раздел свёрнут при открытии; если команд не было — текст «команд по задаче не было». На ширине от 900 px «Связи», «Описание», «Комментарии» — в левой колонке, заметки, прочие поля и команды — в правой; уже — одна колонка: заметки, связи, описание, комментарии, прочие поля, команды. Если задачи нет в зеркале (404), окно SHALL сообщать об этом с кнопкой «Закрыть»; хаб не отвечает — сообщение и «Повторить», ранее загруженные данные остаются.
Scenario: Связанная задача
- WHEN у AURORA-1620 исходящая связь
ASUPR_Блокер_Началана AURORA-1621 «Расчёт свода» в статусе «В работе» - THEN в карточке в группе «Блокирует» строка «начало · AURORA-1621 · Расчёт свода · В работе»; щелчок по ключу
меняет содержимое окна на AURORA-1621, адрес —
/app/issues/AURORA-1621; «назад» браузера возвращает AURORA-1620
Scenario: Входящий блокер
- WHEN у AURORA-1620 входящая связь
ASUPR_Блокер_Завершенияот AURORA-1500 - THEN строка с AURORA-1500 и меткой «завершение» — в группе «Зависит от»
Scenario: Задача без команд
- WHEN по AURORA-1620 не было команд
- THEN в разделе «Команды моста» — текст «команд по задаче не было», таблицы нет
Scenario: Закрытие после переходов
- WHEN со списка «Задачи» открыта AURORA-1620, в окне — переход по связи на AURORA-1621, нажата Esc
- THEN окно закрыто, адрес — адрес списка с прежними фильтрами; «назад» браузера не открывает карточку
Scenario: Прямой адрес
- WHEN в новой вкладке открыт
/app/issues/AURORA-1620, затем нажат ✕ - THEN под окном был список задач без фильтров; после закрытия адрес
/app/issues, «назад» не открывает карточку
Scenario: Скрытые комментарии
- WHEN в карточке нажато «Скрыть» у комментариев, затем страница перезагружена и открыта другая карточка
- THEN комментарии в ней скрыты, видна кнопка «Показать» и число комментариев
Scenario: Просрочка закрытой задачи
- WHEN у AURORA-1620 срок 2026-10-01, сегодня 2026-10-17, статус Jira «Готово»
- THEN в строке под темой «срок 01.10» без пометки «просрочено»; при статусе «В работе» — с пометкой
Requirement: Служебные заметки
Карточка задачи SHALL показывать служебные заметки в режиме чтения: «Вид работы», «Сделано»,
«Осталось», «Заметка владельца» (пустые — «—») и время и автор последнего изменения; кнопка «Править» SHALL открывать
форму: «вид работы» — выбор из значений kind_labels настроек (и текущего значения, если его там нет, и пустого),
«сделано», «осталось», «заметка владельца» —
многострочный текст; «Отмена» SHALL возвращать режим чтения, при несохранённых правках — после подтверждения.
Сохранение SHALL отправлять PUT /issues/{key}/note только с изменёнными полями и updated_by: "web"; очищенное поле
отправляется как null. После успеха SHALL показываться режим чтения с сохранённой заметкой; список задач под окном SHALL
показывать новый «вид работы» без «Перечитать», доска под окном SHALL перечитать снимок
(GET /board) без потери раскладки и прокрутки; при 422 — текст ошибки у формы без
потери введённого. Закрытие окна, переход по связи и уход в другой раздел с несохранёнными правками SHALL требовать
подтверждения. Статуса по факту в заметках приложения нет: форма его не показывает и не отправляет, сохранённое в хабе значение не
меняется. Заметки пишутся только в хаб, в Jira приложение ничего не отправляет.
Scenario: Только изменённые поля
- WHEN в заметке нажато «Править», изменено только «осталось» и нажато «Сохранить»
- THEN тело
PUT /issues/{key}/note—{rest_text: "…", updated_by: "web"}; карточка снова в режиме чтения с новым «Осталось»
Scenario: Уход с несохранёнными правками
- WHEN в форме заметки есть несохранённая правка и нажата Esc
- THEN приложение спрашивает подтверждение; при отказе окно открыто, форма с введённым текстом
Scenario: Заметка видна на доске
- WHEN карточка AURORA-9002 открыта с доски, вид работы изменён на «тестирование» и сохранён
- THEN доска под окном остаётся на месте и перезапрашивается (
GET /boardотдаётwork_kind«тестирование»); окно закрывается без потери раскладки и прокрутки доски
Requirement: Сборка и разработка
Приложение SHALL быть npm-проектом в web/ (React, TypeScript, Vite) с точными версиями зависимостей и
package-lock.json. make web SHALL устанавливать зависимости (npm ci) и собирать приложение в
src/jira_hub/web/app/; каталог сборки не хранится в git. Шрифты (Mulish — текст интерфейса, IBM Plex Mono — ключи
задач и исходники), иконки и прочие ресурсы страницы SHALL входить в сборку: приложение не обращается к внешним
адресам. make check SHALL запускать uv run pytest, проверку типов и линтер фронта и тесты фронта; зелёным считается
только весь набор. npm run dev SHALL поднимать сервер разработки с прокси маршрутов API на хаб из JIRA_HUB_URL (по
умолчанию тестовый http://127.0.0.1:8791).
Scenario: Сборка с нуля
- WHEN в чистом checkout выполнен
make web - THEN в
src/jira_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}— моноширинный блок без разбора разметки внутри;{quote}…{quote}и строкаbq.— цитата; открывающий тег блока — в начале строки, закрывающий — в любом месте той же или последующей строки, текст после закрывающего тега разбирается дальше как обычные строки; тег не в начале строки — текст как есть; строка----— горизонтальная черта; прочие строки — абзацы, разделённые пустыми строками, одиночный перенос внутри абзаца — перенос строки; - внутри строки:
*жирный*,_курсив_,-зачёркнутый-,+подчёркнутый+,^верхний^,~нижний~,{{моно}}({{…}}— без разбора внутри); маркер выделения действует, только если открывающий стоит в начале текста или после пробела либо знака препинания и за ним не пробел и не тот же символ (**x**— текст как есть), а закрывающий стоит после не-пробела и за ним не буква и не цифра (data_type,2026-10-15,счёт-фактура,a*b*c— обычный текст);\\— перенос строки;\перед символом разметки — сам символ; - ссылки:
[текст|адрес],[адрес]и голые адресаhttp://,https://— ссылка в новой вкладке, только для схемhttp,https,mailto, иначе текст как есть;[ключ задачи],[текст|ключ задачи]и ключ задачи в тексте видаПРОЕКТ-123, гдеПРОЕКТ—project_keyиз настроек хаба, — ссылка, открывающая карточку этой задачи в окне (UTF-8,SHA-256и ключи других проектов — текст);[~логин]— «@логин». Всё, что разборщик не распознал ({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 текст показан как есть, без курсива, зачёркивания, жирного и ссылок
Requirement: Проект в приложении
Доска и список задач SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром project в
адресе /app/board и /app/issues; без параметра — последний выбранный в этом браузере (localStorage), если он есть
в GET /projects, иначе первый из GET /projects; выбранный проект записывается в адрес без новой записи истории.
Проекта из адреса нет в GET /projects — страница SHALL показывать «проект <метка> не найден» и выпадающий список
проектов вместо данных. Проектов нет — доска и список SHALL показывать приглашение создать проект («+ Новый проект…»).
Переходы «Доска связей» / «Задачи» SHALL сохранять текущий проект. Карточка задачи (/app/issues/{key}) от проекта не зависит.
Доска SHALL загружать GET /board?project=<текущий>; «Обновить из Jira» — POST /refresh {full: true}, обновляет все
проекты. Раскладка, дорожки и фильтры доски в localStorage SHALL храниться отдельно для каждого проекта;
у проекта AWPCA — на прежних ключах (раскладка, сделанная до проектов, сохраняется). Фильтр спринтов SHALL
показываться, только если хотя бы у одной своей задачи снимка (neighbor = false) есть спринт; иначе — «колонки
«Спринт» нет — показаны все задачи».
Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка,
название, номер доски Jira (необязательно; подсказка «scrum или kanban определится сам»). «Создать» — POST /projects
с автором viewer; ошибка хаба 409/422 — текст detail рядом с кнопкой, окно остаётся открытым; успех — окно
закрывается, текущим становится новый проект, боковая панель показывает ход пакета обновления из ответа (batch_id).
Scenario: Первый вход после выкатки
- WHEN открыт
/app/boardбезproject, вlocalStorageвыбора нет, проектыAWPCAиSTOCKS_DRRK - THEN адрес —
/app/board?project=AWPCA, доска —GET /board?project=AWPCAс прежней раскладкой карточек
Scenario: Переключение проекта
- WHEN на доске
AWPCAв выпадающем списке выбранSTOCKS_DRRK, затем открыт раздел «Задачи» - THEN доска —
GET /board?project=STOCKS_DRRKсо своей раскладкой; адрес «Задач» —/app/issues?project=STOCKS_DRRK
Scenario: Неизвестный проект в адресе
- WHEN открыт
/app/issues?project=NOPE - THEN «проект NOPE не найден» и список проектов; запроса
GET /issuesнет
Scenario: Создание проекта из формы
- WHEN в форме введены
STOCKS_DRRK, «Запасы ДРРК», доска пусто; нажато «Создать» - THEN
POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", author: "viewer"}; текущий проект —STOCKS_DRRK; боковая панель показывает «пакет #N выполняется»
Scenario: Метка занята
- WHEN в форме введена метка
awpca, хаб ответил 409 - THEN окно открыто, рядом с «Создать» — текст ошибки хаба
Scenario: Канбан-проект
- WHEN текущий проект —
STOCKS_DRRK, ни у одной своей задачи снимка нет спринта (у соседа изAWPCAспринт есть) - 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: Обновление из Jira
Действие «Обновить из Jira» SHALL быть в боковой панели приложения и доступно с любой страницы. Оно SHALL проверять
агента (GET /agent; офлайн — предупреждение, пакет всё равно ставится), ставить POST /refresh {full: true, author: "viewer"} и опрашивать GET /batches/{id} каждые 3 с одним опросом до терминального статуса пакета. Кнопка SHALL быть
недоступна (с признаком загрузки), пока запрос /refresh в пути или пакет выполняется. Незавершённый пакет автора
viewer, первая команда которого — refresh, SHALL подхватываться при входе в приложение и после каждой успешной
загрузки снимка доски, кроме перечитывания снимка после завершения собственного пакета; пакеты viewer с другими
командами не подхватываются. Опрос SHALL прекращаться при ответе 4xx (404 — «пакета нет в хабе») и после 5 ошибок
«хаб не отвечает» подряд, с сообщением и снова доступной кнопкой. По завершении пакета приложение SHALL перечитывать
снимки досок, списки задач, открытую карточку и список проектов; доска, показывающая снимок из файла, его не заменяет.
Пакет, поставленный формой «Новый проект», SHALL опрашиваться так же сразу после создания.
Боковая панель SHALL показывать блок данных Jira: состояние агента («агент на связи» / «агент офлайн», по GET /agent
при входе и раз в 60 с), время последнего полного обновления («данные на ДД.ММ ЧЧ:ММ» по last_sync снимка доски
текущего проекта, «полного обновления не было», если его нет) и одно текущее событие пакета: в очереди / выполняется (с
оставшимся временем, с пометкой «агент офлайн» и «хаб не отвечает, повтор»), выполнен, не выполнен с текстом ошибки,
отменён, опрос остановлен с причиной, хаб не ответил на постановку; новое событие заменяет предыдущее, «выполнен»
исчезает через 10 с.
Scenario: Обновление со списка задач
- WHEN открыт
/app/issues, нажато «Обновить из Jira», пакет выполнен - THEN в хабе один пакет
refreshавтораviewer; список задач перезапрошен; боковая панель показывала «пакет #N выполняется», затем «пакет #N выполнен»
Scenario: Один прогон на всё приложение
- WHEN нажата «Обновить из Jira» и, пока пакет выполняется, выполнен переход между разделами и «Перечитать» на доске
- THEN кнопка остаётся недоступной, в хабе один пакет, его опрашивает один опрос
Scenario: Опрос получил 404
- WHEN опрос пакета получил 404
- THEN опрос остановлен, боковая панель показывает «пакета нет в хабе», кнопка «Обновить из Jira» снова доступна
Scenario: Пакет с другой вкладки
- WHEN пакет
viewerс командойrefreshпоставлен с другой вкладки и не завершён, приложение открыто - THEN боковая панель показывает «пакет #N выполняется» и по завершении данные перечитываются
Scenario: Пакет с другими командами
- WHEN незавершённый пакет автора
viewerсостоит из командensure_fields - THEN приложение его не опрашивает, кнопка «Обновить из Jira» доступна
Scenario: Агент офлайн
- WHEN
GET /agentотвечаетonline: falseи нажато «Обновить из Jira» - THEN пакет поставлен, событие — «агент офлайн — пакет выполнится, когда он выйдет на связь; пакет #N в очереди…»
Requirement: Тема
Переключатель темы SHALL быть в боковой панели приложения: два положения — светлая и тёмная. Выбор SHALL храниться в
localStorage под ключом graph-theme (light / dark), как у прежней доски; без сохранённого выбора или со
значением "" тема SHALL следовать настройке системы, и переключатель показывает её. Тема применяется до первой
отрисовки, без мигания.
Scenario: Выбор темы сохраняется
- WHEN в системе светлая тема, переключатель переведён в «тёмная», страница перезагружена
- THEN приложение в тёмной теме, в
localStoragegraph-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 символов текста одной строкой с «…»; прочие поля — «<название поля>: «было» → «стало»» (значения до 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 местного времени
Requirement: История в карточке задачи
Карточка задачи SHALL показывать в основной колонке после комментариев блок «История (N)» — события changes из
GET /issues/{key} (все виды, новые сверху) в том же виде, что в ленте, всегда с датой. Видны 10 последних событий,
остальные — по кнопке «Показать все (N)»; без событий — подсказка «изменений нет»; ответ хаба без поля changes —
блока нет.
Scenario: Карточка с историей
- WHEN открыта карточка задачи с 12 событиями журнала
- THEN блок «История (12)» показывает 10 последних событий и кнопку «Показать все (12)»; после нажатия — все 12