chore(openspec): архив web-modules (#98)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-24 15:58:18 +04:00
parent b92d5e8582
commit 3094e364a1
7 changed files with 256 additions and 111 deletions

View File

@ -417,7 +417,7 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report
- любой другой `GET /app` или `GET /app/…` — `index.html` сборки, `text/html; charset=utf-8`, `Cache-Control: no-cache`;
- если `index.html` сборки нет — на любой `GET /app…` 503 `text/html; charset=utf-8` с текстом, что приложение не
собрано и нужно выполнить `make web` в каталоге sdlc-hub;
- `GET /` и `GET /links` — 302 с `Location: /app/board`.
- `GET /` и `GET /links` — 302 с `Location: /app/` (главная страница приложения — «Обзор»).
Сборка приложения не хранится в git; её создаёт `make web`.
#### Scenario: Файл сборки
@ -438,7 +438,7 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report
#### Scenario: Старый адрес доски
- **WHEN** запрошен `GET /links` или `GET /`
- **THEN** 302, `Location: /app/board`
- **THEN** 302, `Location: /app/`
#### Scenario: Нулевой байт в пути
- **WHEN** запрошен `GET /app/assets/%00` или `GET /app/assets/a%00.js`
@ -485,15 +485,16 @@ sha256 (без расширения и имени вложения: имя за
Хаб SHALL предоставлять под клиентским токеном:
- `GET /projects` → список проектов по `created_at`, затем `label`; каждый — плоский объект из полей ядра (`label`,
`name`, `created_at`) и полей всех модулей (`modules`, «Поля проекта от модулей»). Модуль jira даёт `board_id`,
`board_type`, `issues_count` (задачи зеркала с меткой проекта), модуль wiki — `wiki_root`, `wiki_root_title`,
`board_type`, `issues_count` (задачи зеркала с меткой проекта), `jira_synced_at` (время завершения последнего полного
обновления зеркала, общее для всех проектов; `null`, если его не было), модуль wiki — `wiki_root`, `wiki_root_title`,
`wiki_pages`, `wiki_synced_at`, `wiki_sync_error`.
- `POST /projects` `{label, name, author?, <поля модулей>}` → 201 `{project, batch_id}`: в одной транзакции проект
записывается, модули записывают свои поля и ставят свои пакеты; `batch_id` — пакет первого модуля, вернувшего пакет
(у jira — «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку). Автор пакетов —
- `POST /projects` `{label, name, author?, <поля модулей>}` → 201 `{project, batch_id, batches}`: в одной транзакции
проект записывается, модули записывают свои поля и ставят свои пакеты; `batches` — список `{module, id}` всех
поставленных пакетов в порядке модулей; `batch_id` — пакет первого модуля, вернувшего пакет (у jira — «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку). Автор пакетов —
`author` (по умолчанию `api`). Неизвестное поле, неверные поля ядра (см. `data-model`, «Проекты») или модуля — 422 с
указанием поля; метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакетов нет.
- `PATCH /projects/{label}` `{<изменяемые поля модулей>, author?}` → 200 `{project, batch_ids}`: модули правят свои поля
и ставят пакеты в одной транзакции. Неизменяемое поле (метка, название, `board_id`) — 422 «<поле> после создания не
- `PATCH /projects/{label}` `{<изменяемые поля модулей>, author?}` → 200 `{project, batch_ids, batches}`: модули правят
свои поля и ставят пакеты в одной транзакции; `batches` — список `{module, id}`. Неизменяемое поле (метка, название, `board_id`) — 422 «<поле> после создания не
меняется»; неизвестное поле или неверное значение — 422 с указанием поля; проекта нет — 404.
#### Scenario: Новый проект
@ -508,7 +509,7 @@ sha256 (без расширения и имени вложения: имя за
#### Scenario: Проект с корнем вики
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234"}`
- **THEN** 201; в очереди пакет `refresh` и пакет `wiki_sync {label: "STOCKS_DRRK", root: "DOC-001234"}`; `batch_id` —
пакет `refresh`
пакет `refresh`; `batches` — `[{module: "jira", id: <refresh>}, {module: "wiki", id: <wiki_sync>}]`
#### Scenario: Повтор метки
- **WHEN** `POST /projects {label: "AWPCA", name: "…"}`, проект `AWPCA` есть
@ -524,12 +525,16 @@ sha256 (без расширения и имени вложения: имя за
#### Scenario: Корень вики у существующего проекта
- **WHEN** `PATCH /projects/AWPCA {wiki_root: "DOC-000777"}`
- **THEN** 200, `batch_ids` — один пакет `wiki_sync`; `GET /projects` отдаёт `AWPCA` с `wiki_root = DOC-000777`
- **THEN** 200, `batch_ids` — один пакет `wiki_sync`, `batches` — `[{module: "wiki", id: <он же>}]`; `GET /projects` отдаёт `AWPCA` с `wiki_root = DOC-000777`
#### Scenario: Доску не меняют
- **WHEN** `PATCH /projects/AWPCA {board_id: 1}`
- **THEN** 422 «board_id после создания не меняется», доска прежняя
#### Scenario: Свежесть Jira в проекте
- **WHEN** последний полный пакет `refresh` завершён в 11:07, `GET /projects`
- **THEN** у каждого проекта `jira_synced_at` — это время; до первого полного обновления — `null`
### Requirement: Приём журнала изменений
Хаб SHALL принимать от агента журнал изменений задач маршрутом `POST /agent/history` (агентский токен) с телом
`{command_id, changes: [...]}`, элемент — `{history_id, seq, key, created, author, field_id, field_name, from,
@ -714,3 +719,20 @@ from_string, to, to_string}`. До записи хаб SHALL проверить
#### Scenario: Репозиторий в Gitea не переименован
- **WHEN** `bash scripts/cutover.sh --apply`, `aep/sdlc-hub` в Gitea не найден
- **THEN** скрипт останавливается на шаге 1 с подсказкой, launchd и каталоги не тронуты
### Requirement: Модуль пакета
`GET /batches` и `GET /batches/{id}` SHALL отдавать у пакета поле `module` — имя модуля, которому принадлежит тип первой
команды пакета (по описаниям модулей в реестре); пакет без команд или с типом, которого не знает ни один модуль, —
`module = null`. Фильтр `GET /batches?module=<имя>` SHALL отдавать только пакеты модуля; неизвестный модуль — 404.
#### Scenario: Модуль у пакетов
- **WHEN** в очереди пакет «Автообновление» (`refresh`, автор `hub`) и пакет `wiki_sync` проекта `AWPCA`; `GET /batches`
- **THEN** у первого `module = "jira"`, у второго `module = "wiki"`
#### Scenario: Фильтр по модулю
- **WHEN** `GET /batches?module=wiki&project=AWPCA`
- **THEN** только пакеты модуля wiki проекта `AWPCA`
#### Scenario: Неизвестный модуль
- **WHEN** `GET /batches?module=nope`
- **THEN** 404

View File

@ -40,29 +40,43 @@ SHALL очищать токен и показывать экран входа.
- **THEN** экран входа показывает текст `detail`, а не «хаб не отвечает»
### Requirement: Оболочка и адреса
Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
задач), `/app/feed` (лента изменений, требование «Лента изменений») и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
на страницу «не найдено» со ссылками на разделы. Боковая панель SHALL быть закреплена у левого края окна на всю его
высоту и MUST NOT прокручиваться вместе со страницей; страница занимает остальную ширину. Сверху вниз панель SHALL
содержать: название «SDLC Hub» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий
отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»); разделы «Доска связей», «Задачи» и «Лента» — вертикальным
списком ссылок со значками (текущий отмечен `aria-current="page"`); внизу — блок данных Jira с действием «Обновить из
Jira» (требование «Обновление из Jira»), переключатель темы (требование «Тема») и меню с действием «Сменить токен».
Свёрнутая панель SHALL быть узкой полосой с теми же действиями значками: «Развернуть панель», метка текущего проекта,
разделы, «Обновить из Jira» с точкой состояния агента, тема, меню; названия — в подсказках и `aria-label`. Свёртка
SHALL храниться в `localStorage` под ключом `sdlc-hub-sidebar` (`collapsed`; без записи — развёрнута). На экранах уже
640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», названием и кнопкой «Обновить из
Jira»; «Меню» открывает развёрнутую панель поверх страницы, панель закрывается выбором раздела, клавишей Escape или
щелчком мимо неё. Горизонтальной прокрутки страницы на любой ширине нет.
Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не пустым экраном.
Приложение 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** на доске в боковой панели выбран раздел «Задачи»
- **THEN** адрес — `/app/issues` с текущим проектом, показан список задач; «назад» возвращает на доску
- **WHEN** на доске в боковой панели выбран раздел «Задачи» группы «Jira»
- **THEN** адрес — `/app/jira/issues` с текущим проектом, показан список задач; «назад» возвращает на доску
#### Scenario: Раздел «Лента»
- **WHEN** на доске в боковой панели выбран раздел «Лента»
- **THEN** адрес — `/app/feed` с текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели
- **THEN** адрес — `/app/jira/feed` с текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели
#### Scenario: Сменить токен
- **WHEN** в меню боковой панели выбрано «Сменить токен»
@ -70,16 +84,25 @@ Jira»; «Меню» открывает развёрнутую панель по
#### Scenario: Панель на месте при прокрутке
- **WHEN** доска прокручена до самого низа
- **THEN** боковая панель видна целиком на прежнем месте: разделы и «Обновить из Jira» доступны без прокрутки вверх
- **THEN** боковая панель видна целиком на прежнем месте: разделы и блок состояния модулей доступны без прокрутки вверх
#### Scenario: Свёрнутая панель
- **WHEN** нажата «Свернуть панель», страница перезагружена
- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; «Развернуть панель»
возвращает полную панель
- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; видны два значка
состояния — Jira и Вики; «Развернуть панель» возвращает полную панель
#### Scenario: Узкий экран
- **WHEN** ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи»
- **THEN** панель открылась поверх страницы, после выбора закрыта, показан список задач; горизонтальной прокрутки нет
- **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) — сообщение
@ -461,47 +484,51 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха
234,4 КБ»; щелчок по любой из них скачивает файл
### Requirement: Проект в приложении
Доска, список задач и вики SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром
`project` в адресе `/app/board`, `/app/issues` и `/app/wiki`; без параметра — последний выбранный в этом браузере
(`localStorage`), если он есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес
без новой записи истории. Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден»
и выпадающий список проектов вместо данных. Проектов нет — доска, список и вики SHALL показывать приглашение создать
проект («+ Новый проект…»). Переходы между «Доска связей», «Задачи» и «Вики» SHALL сохранять текущий проект. Карточка
задачи (`/app/issues/{key}`) от проекта не зависит.
«Обзор», доска, список задач, лента и вики SHALL показывать один проект — текущий. Текущий проект SHALL задаваться
параметром `project` в адресе страниц; без параметра — последний выбранный в этом браузере (`localStorage`), если он
есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории.
Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и подсказку выбрать
проект в списке «Проект» боковой панели (на узком экране — в «Меню») вместо данных. Проектов нет — страницы SHALL
показывать приглашение создать проект («+ Новый проект…»). Переходы между страницами SHALL сохранять текущий проект;
смена проекта на странице, которой нет у нового проекта, открывает «Обзор». Карточка задачи
(`/app/jira/issues/{key}`) от проекта не зависит.
Доска SHALL загружать `GET /board?project=<текущий>`; «Обновить из Jira» — `POST /refresh {full: true}`, обновляет все
проекты. Раскладка, дорожки и фильтры доски в `localStorage` SHALL храниться отдельно для каждого проекта;
у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов, сохраняется). Фильтр спринтов SHALL
показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`) есть спринт; иначе — «колонки
«Спринт» нет — показаны все задачи».
Доска SHALL загружать `GET /board?project=<текущий>`. Раскладка, дорожки и фильтры доски в `localStorage` SHALL
храниться отдельно для каждого проекта; у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов,
сохраняется). Фильтр спринтов SHALL показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`)
есть спринт; иначе — «колонки «Спринт» нет — показаны все задачи».
Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка,
название и поля модулей — номер доски Jira (необязательно; подсказка «scrum или kanban определится сам») и корень вики
(необязательно; подсказка «DOC-… или ссылка на страницу Eva»). Пустое необязательное поле в запрос не попадает.
«Создать» — `POST /projects` с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с кнопкой, окно остаётся
открытым; успех — окно закрывается, текущим становится новый проект, боковая панель показывает ход пакета обновления из
ответа (`batch_id`).
Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка (с
подсказкой «метка задач в Jira и имя проекта в хабе») и название, затем поля каждого модуля в своей группе с
заголовком — названием модуля: «Jira» — номер доски Jira (необязательно; подсказка «scrum или kanban определится
сам»), «Вики» — корень вики (необязательно; подсказка «DOC-… или ссылка на страницу Eva»). Подсказка — текстом под
полем, не в placeholder. Пустое необязательное поле в запрос не попадает; числовое поле с нецелым значением — ошибка у
поля, запрос не уходит. «Создать» — `POST /projects` с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с
кнопкой, окно остаётся открытым; успех — окно закрывается, текущим становится новый проект, каждый пакет из ответа
(`batches`) отслеживается своим модулем (требование «Состояние и обновление модулей»).
#### Scenario: Первый вход после выкатки
- **WHEN** открыт `/app/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK`
- **THEN** адрес — `/app/board?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек
- **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/issues?project=STOCKS_DRRK`
- **THEN** доска — `GET /board?project=STOCKS_DRRK` со своей раскладкой; адрес «Задач» —
`/app/jira/issues?project=STOCKS_DRRK`
#### Scenario: Неизвестный проект в адресе
- **WHEN** открыт `/app/issues?project=NOPE`
- **THEN** «проект NOPE не найден» и список проектов; запроса `GET /issues` нет
- **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`;
боковая панель показывает «пакет #N выполняется»
строка «Jira» блока состояния показывает «пакет #N выполняется»
#### Scenario: Проект с корнем вики из формы
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», корень `DOC-001234`; нажато «Создать»
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234", author: "viewer"}`
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234", author: "viewer"}`; строка
«Jira» показывает пакет обновления, строка «Вики» — пакет синхронизации
#### Scenario: Метка занята
- **WHEN** в форме введена метка `awpca`, хаб ответил 409
@ -511,6 +538,10 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха
- **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, радиусы, высоты
@ -534,50 +565,6 @@ Semibold 14/20, заголовок страницы Bold 20/28. Страницы
- **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`), как у прежней доски; без сохранённого выбора или со
@ -671,19 +658,22 @@ Semibold 14/20, заголовок страницы Bold 20/28. Страницы
- **THEN** под вложением этот текст и «Повторить»
### Requirement: Вики проекта
Приложение SHALL иметь страницу «Вики» (`/app/wiki?project=<метка>`, пункт боковой панели; текущий проект — как у доски
и задач, «Проект в приложении»). Страница SHALL показывать:
- дерево страниц проекта (`GET /wiki/tree`) слева, свёрнутое до первого уровня; узел раскрывается по клику;
Приложение SHALL иметь страницу «Вики» (`/app/wiki?project=<метка>`, раздел группы «Вики» боковой панели; текущий
проект — как у остальных страниц, «Проект в приложении»). Шапка страницы: заголовок «Вики», подзаголовок «<название
корня> · <N> страниц · обновлено ДД.ММ ЧЧ:ММ» и действие «Обновить вики» (вторичная кнопка; требование «Состояние и
обновление модулей»). Страница SHALL показывать:
- дерево страниц проекта (`GET /wiki/tree`) слева, свёрнутое до первого уровня; узел раскрывается по клику; узел
выбранной страницы и его предки раскрыты;
- выбранную страницу справа (`GET /wiki/pages/{code}`): заголовок, дата синхронизации, ссылка «открыть в Eva»,
Markdown с таблицами, без сырого HTML; ссылки `page:<код>` открывают страницу вики (код в адресе `&page=<код>`),
ссылки `attachment:<id>` скачивают вложение (запрос, опрос состояния, сохранение файла браузером), состояние
`failed` — текст ошибки рядом со ссылкой;
Markdown с таблицами, без сырого HTML; без `page` в адресе — корневая страница; ссылки `page:<код>` открывают страницу
вики (код в адресе `&page=<код>`), ссылки `attachment:<id>` скачивают вложение (запрос, опрос состояния, сохранение
файла браузером), состояние `failed` — текст ошибки рядом со ссылкой;
- на экранах уже 640 px выбранная страница — над деревом;
- поиск над деревом (`GET /wiki/search`): список результатов с фрагментами вместо дерева, пока строка поиска не пуста;
- кнопку «Обновить вики» (`POST /wiki/sync`) и плашку хода синхронизации в боковой панели; ответ 409 — плашка текущего
пакета;
после очистки поиска раскрытие дерева прежнее;
- проекта без корня — форму «Корень вики (DOC-… или ссылка)» с кнопкой «Привязать» (`PATCH /projects/{label}`); 422 —
текст ошибки у поля;
- корень есть, синхронизаций нет — «вики ещё не синхронизирована» и кнопку «Обновить вики»;
- корень есть, синхронизаций нет — «вики ещё не синхронизирована» и действие «Обновить вики»;
- последняя синхронизация с ошибкой — плашку с текстом ошибки над страницей.
#### Scenario: Открыть страницу по ссылке
@ -692,8 +682,141 @@ Semibold 14/20, заголовок страницы Bold 20/28. Страницы
#### Scenario: Проект без корня
- **WHEN** открыта `/app/wiki?project=sfm_kedr`, корня нет; введено `DOC-001234`, нажато «Привязать»
- **THEN** `PATCH /projects/sfm_kedr {wiki_root: "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** тесты фронта падают с адресом и модулями