## MODIFIED Requirements ### Requirement: Оболочка и адреса Приложение SHALL открываться под префиксом `/app/`. Главная страница — «Обзор» ядра (`/app/?project=<метка>`, требование «Обзор проекта»). Страницы модулей SHALL лежать под префиксом имени модуля: модуль jira — `/app/jira/board` (доска связей), `/app/jira/issues` (список задач), `/app/jira/feed` (лента изменений, требование «Лента изменений») и `/app/jira/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); модуль wiki — `/app/wiki` (требование «Вики проекта»). Прежние адреса `/app/board`, `/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** на доске в боковой панели выбран раздел «Задачи» группы «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 показывать один проект — текущий. Текущий проект SHALL задаваться параметром `project` в адресе страниц; без параметра — последний выбранный в этом браузере (`localStorage`), если он есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории. Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и подсказку выбрать проект в списке «Проект» боковой панели (на узком экране — в «Меню») вместо данных. Проектов нет — страницы SHALL показывать приглашение создать проект («+ Новый проект…»). Переходы между страницами SHALL сохранять текущий проект; смена проекта на странице, которой нет у нового проекта, открывает «Обзор». Карточка задачи (`/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/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK` - **THEN** адрес — `/app/jira/board?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 иметь страницу «Вики» (`/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** справа корневая страница проекта, в дереве она выделена ## REMOVED Requirements ### Requirement: Обновление из Jira **Reason**: обновление и ход пакетов стали общими для модулей. **Migration**: требование «Состояние и обновление модулей»; поведение «Обновить из Jira» (одна кнопка на приложение, `POST /refresh {full: true, author: "viewer"}`, один опрос, подхват, остановка, перечитывание) сохраняется в нём для модуля jira. ## ADDED Requirements ### 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** тесты фронта падают с адресом и модулями