# 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 быть доступна, пока последняя порция полная. #### 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)» — новые сверху: значок по типу файла (картинка, таблица, PDF, прочее), имя, размер, «автор · дата» и кнопка скачивания (требование «Скачивание вложений в карточке»); без вложений раздела нет; - «Комментарии (N)» — новые сверху, автор, время, кнопка «исходник» / «текст» у каждого комментария, текст, отрендеренный по требованию «Разметка Jira в карточке», под текстом — вложения, на которые ссылается комментарий (`attachment_ids`): строка «плашек» с именем и размером, щелчок — скачивание; кнопка «Скрыть» / «Показать» сворачивает раздел, выбор 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/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}` — моноширинный блок без разбора разметки внутри; `{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 КБ»; щелчок по любой из них скачивает файл ### Requirement: Проект в приложении «Обзор», доска, список задач, лента и вики SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром `project` в адресе страниц; без параметра — последний выбранный в этом браузере (`localStorage`), если он есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории. Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и подсказку выбрать проект в списке «Проект» боковой панели (на узком экране — в «Меню») вместо данных. Проектов нет — страницы SHALL показывать приглашение создать проект («+ Новый проект…»). Переходы между страницами 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` нет ### 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 символов текста одной строкой с «…»; прочие поля — «<название поля>: «было» → «стало»» (значения до 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 ### 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` и прочие ошибки хаба — их текст там же. Вложение с `state = ready` в разделе «Вложения» помечается «у хаба» и скачивается сразу. Размер неизвестен — «размер неизвестен» и обычная кнопка. Вложение больше 10 МБ — вместо кнопки ссылка «Открыть в Jira» (`<адрес Jira>/secure/attachment//<имя в 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** под вложением этот текст и «Повторить» ### Requirement: Вики проекта Приложение SHALL иметь страницу «Вики» (`/app/wiki?project=<метка>`, раздел группы «Вики» боковой панели; текущий проект — как у остальных страниц, «Проект в приложении»). Шапка страницы: заголовок «Вики», подзаголовок «<название корня> · страниц · обновлено ДД.ММ ЧЧ:ММ» и действие «Обновить вики» (вторичная кнопка; требование «Состояние и обновление модулей»). Страница SHALL показывать: - дерево страниц проекта (`GET /wiki/tree`) слева, свёрнутое до первого уровня; узел раскрывается по клику; узел выбранной страницы и его предки раскрыты; - выбранную страницу справа (`GET /wiki/pages/{code}`): заголовок, дата синхронизации, ссылка «открыть в Eva», Markdown с таблицами, без сырого HTML; без `page` в адресе — корневая страница; ссылки `page:<код>` открывают страницу вики (код в адресе `&page=<код>`), ссылки `attachment:` скачивают вложение (запрос, опрос состояния, сохранение файла браузером), состояние `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 ошибок «хаб не отвечает» подряд — опрос остановлен с сообщением; по завершении — перечитать данные модуля и список проектов; - ответ 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` в подсказке ### 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» — меню «Колонки». Если задан хотя бы один фильтр (поиск, исполнитель, эпик, «Скрыть выполненные») — кнопка «Сбросить фильтры», снимающая их все. Фильтры действуют вместе (И). Сохранённое значение исполнителя или эпика, которого нет в данных, 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=[,…]` (переход без новой записи истории); идентификаторы, которых нет среди активных и будущих, отбрасываются, если не осталось ни одного — все активные. Над доской — заголовок каждого выбранного спринта (имя, даты, «осталось 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** колонки «Выполнено» нет, на кнопке «Колонки» — 1; остальные колонки на месте #### Scenario: Широкая доска - **WHEN** окно 2560 × 1300 px, у доски 12 колонок, в одной из них 40 карточек - **THEN** все колонки одной высоты и помещаются в окно по высоте; полоса горизонтальной прокрутки доски видна в окне; прокрутка доски вправо до конца показывает «Без колонки» целиком; страница горизонтально не прокручивается