Merge pull request 'docs(openspec): архив web-app-followups (#29)' (#34) from chore/29-archive-web-app-followups into main

This commit is contained in:
aep 2026-10-15 09:23:03 +03:00
commit 55886bd258
7 changed files with 57 additions and 9 deletions

View File

@ -265,8 +265,8 @@ sha256 <первые 12 символов>…, <size> байт»; при ошиб
### Requirement: Раздача веб-приложения ### Requirement: Раздача веб-приложения
Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/jira_hub/web/app/`, читая файлы Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/jira_hub/web/app/`, читая файлы
с диска на каждый запрос: с диска на каждый запрос:
- `GET /app/assets/<путь>` — файл из `assets/` сборки; путь, который после разрешения выходит за `assets/` или не - `GET /app/assets/<путь>` — файл из `assets/` сборки; путь, который после разрешения выходит за `assets/`, не
существует, — 404; `Content-Type` по расширению (`.js` — `text/javascript; charset=utf-8`, `.css` — существует или содержит нулевой байт, — 404; `Content-Type` по расширению (`.js` — `text/javascript; charset=utf-8`, `.css` —
`text/css; charset=utf-8`, `.svg` — `image/svg+xml`, `.png` — `image/png`, `.ico` — `image/x-icon`, `.woff2` — `text/css; charset=utf-8`, `.svg` — `image/svg+xml`, `.png` — `image/png`, `.ico` — `image/x-icon`, `.woff2` —
`font/woff2`, `.map` — `application/json`, прочее — `application/octet-stream`); `font/woff2`, `.map` — `application/json`, прочее — `application/octet-stream`);
`Cache-Control: public, max-age=31536000, immutable`; `Cache-Control: public, max-age=31536000, immutable`;
@ -295,3 +295,7 @@ sha256 <первые 12 символов>…, <size> байт»; при ошиб
#### Scenario: Старый адрес доски #### Scenario: Старый адрес доски
- **WHEN** запрошен `GET /links` или `GET /` - **WHEN** запрошен `GET /links` или `GET /`
- **THEN** 302, `Location: /app/board` - **THEN** 302, `Location: /app/board`
#### Scenario: Нулевой байт в пути
- **WHEN** запрошен `GET /app/assets/%00` или `GET /app/assets/a%00.js`
- **THEN** 404 `not_found`, а не 500

View File

@ -11,7 +11,9 @@
Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с
подсказкой `uv run jira-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода. подсказкой `uv run jira-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода.
Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен» Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен»
SHALL очищать токен и показывать экран входа. SHALL очищать токен и показывать экран входа. Ответ 401 SHALL выводить на экран входа, только если запрос ушёл с текущим токеном: запоздавший 401 на
запрос с прежним токеном после входа с новым не учитывается. Если проверка токена получила ответ 4xx, кроме 401,
экран входа SHALL показывать текст ошибки хаба, а не «хаб не отвечает».
#### Scenario: Первый вход #### Scenario: Первый вход
- **WHEN** приложение открыто впервые, токена нет - **WHEN** приложение открыто впервые, токена нет
@ -25,6 +27,14 @@ SHALL очищать токен и показывать экран входа.
- **WHEN** в `localStorage` есть `jira-hub-token`, сохранённый страницей `links.html` - **WHEN** в `localStorage` есть `jira-hub-token`, сохранённый страницей `links.html`
- **THEN** приложение входит с ним без запроса токена - **THEN** приложение входит с ним без запроса токена
#### Scenario: Запоздавший 401 от прежнего токена
- **WHEN** запрос ушёл с прежним токеном, пользователь вошёл с новым, и после этого запрос ответил 401
- **THEN** приложение остаётся на странице, новый токен сохранён
#### Scenario: Хаб отверг проверку токена
- **WHEN** проверка `GET /settings` ответила 400 с `detail`
- **THEN** экран входа показывает текст `detail`, а не «хаб не отвечает»
### Requirement: Оболочка и адреса ### Requirement: Оболочка и адреса
Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
задач) и `/app/issues/{key}` (карточка задачи); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь — задач) и `/app/issues/{key}` (карточка задачи); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
@ -69,6 +79,8 @@ SHALL очищать токен и показывать экран входа.
доски нет — оно в верхней панели приложения. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку доски нет — оно в верхней панели приложения. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку
карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке
захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет. захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет.
При возврате на `/app/board` с другой страницы приложения в той же вкладке доска SHALL восстанавливать прежнюю
прокрутку; после перезагрузки страницы доска открывается сверху.
#### Scenario: Раскладка переживает переход #### Scenario: Раскладка переживает переход
- **WHEN** раскладка карточек сохранена страницей `links.html`, открыта `/app/board` с теми же данными - **WHEN** раскладка карточек сохранена страницей `links.html`, открыта `/app/board` с теми же данными
@ -83,6 +95,10 @@ SHALL очищать токен и показывать экран входа.
- **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку» - **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку»
- **THEN** открыта `/app/issues/AURORA-1620` внутри приложения - **THEN** открыта `/app/issues/AURORA-1620` внутри приложения
#### Scenario: Возврат с карточки
- **WHEN** доска прокручена вниз, нажат значок «Открыть карточку», затем выбран раздел «Доска»
- **THEN** доска показана с той же прокруткой
### Requirement: Источник данных доски ### Requirement: Источник данных доски
Доска SHALL хранить явный источник данных — хаб или файл. Пока показан снимок из файла (CSV или JSON-снимок), ответы Доска SHALL хранить явный источник данных — хаб или файл. Пока показан снимок из файла (CSV или JSON-снимок), ответы
хаба SHALL его не заменять; «Перечитать» SHALL возвращать источник «хаб» и загружать `GET /board` заново без обращения к хаба SHALL его не заменять; «Перечитать» SHALL возвращать источник «хаб» и загружать `GET /board` заново без обращения к
@ -91,11 +107,12 @@ SHALL проверять агента (`GET /agent`; офлайн — преду
`POST /refresh {full: true, author: "viewer"}`, опрашивать `GET /batches/{id}` каждые 3 с одним опросом до `POST /refresh {full: true, author: "viewer"}`, опрашивать `GET /batches/{id}` каждые 3 с одним опросом до
терминального статуса пакета и затем перечитывать `GET /board`. Кнопка «Обновить из Jira» SHALL быть недоступна, терминального статуса пакета и затем перечитывать `GET /board`. Кнопка «Обновить из Jira» SHALL быть недоступна,
пока запрос `/refresh` в пути или пакет выполняется. Незавершённый пакет `refresh` с автором `viewer` SHALL пока запрос `/refresh` в пути или пакет выполняется. Незавершённый пакет `refresh` с автором `viewer` SHALL
подхватываться при каждой успешной загрузке из хаба. Опрос пакета SHALL прекращаться при ответе 404 и после 5 ошибок подхватываться при каждой успешной загрузке из хаба, кроме перечитывания снимка после завершения собственного пакета; пакеты `viewer` с другими командами не подхватываются. Опрос пакета SHALL прекращаться при ответе 404 и после 5 ошибок
«хаб не отвечает» подряд, с сообщением и снова доступной кнопкой. Шапка доски SHALL показывать «хаб не отвечает» подряд, с сообщением и снова доступной кнопкой. Шапка доски SHALL показывать
источник данных (данные хаба на время последнего полного прогона; «из файла») и одно текущее событие (загрузка; пакет источник данных (данные хаба на время последнего полного прогона; «из файла») и одно текущее событие (загрузка; пакет
выполняется / выполнен / ошибка с текстом; хаб не отвечает — прежние данные; агент офлайн — пакет поставлен; ошибка выполняется / выполнен / ошибка с текстом; хаб не отвечает — прежние данные, а при снимке из файла — «на доске снимок из файла»; агент офлайн — пакет поставлен; ошибка
разбора файла — «файл не разобран: <причина>»); новое событие заменяет предыдущее целиком. разбора файла — «файл не разобран: <причина>»); новое событие заменяет предыдущее целиком. Пока пакет выполняется,
«Перечитать» и загрузка файла SHALL не стирать событие пакета.
#### Scenario: Поздний ответ хаба не затирает файл #### Scenario: Поздний ответ хаба не затирает файл
- **WHEN** запрос `GET /board` в пути, и в это время загружен CSV - **WHEN** запрос `GET /board` в пути, и в это время загружен CSV
@ -113,12 +130,26 @@ SHALL проверять агента (`GET /agent`; офлайн — преду
- **WHEN** пакет `viewer` поставлен с другой вкладки и не завершён, нажато «Перечитать» - **WHEN** пакет `viewer` поставлен с другой вкладки и не завершён, нажато «Перечитать»
- **THEN** доска показывает «пакет #N выполняется» и по завершении перечитывает данные - **THEN** доска показывает «пакет #N выполняется» и по завершении перечитывает данные
#### Scenario: «Перечитать» во время пакета
- **WHEN** пакет `viewer` выполняется и нажато «Перечитать»
- **THEN** шапка по-прежнему показывает «пакет #N выполняется»
#### Scenario: Хаб недоступен при снимке из файла
- **WHEN** хаб не отвечает и загружен JSON-снимок доски, либо на доске снимок из CSV и хаб не ответил на перечитывание
снимка после завершения пакета
- **THEN** шапка показывает «хаб не отвечает — на доске снимок из файла», а не «показаны прежние данные»
#### Scenario: Чужой пакет не подхватывается
- **WHEN** незавершённый пакет автора `viewer` состоит из команд `ensure_fields`
- **THEN** доска его не опрашивает, кнопка «Обновить из Jira» доступна
### Requirement: Список задач ### Requirement: Список задач
Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (ссылка на карточку), тема, Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (ссылка на карточку), тема,
тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы, тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы,
статус по факту, признак «сосед». Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид статус по факту, признак «сосед». Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид
работы, статус по факту, статус Jira, метка, эпик, только соседи, поиск по ключу или теме), храниться в query-строке работы, статус по факту, статус Jira, метка, эпик, только соседи, поиск по ключу или теме), храниться в query-строке
адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`. Задачи SHALL адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`; значение фильтра из адреса, которого нет среди
вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Задачи SHALL
загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя порция полная. загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя порция полная.
#### Scenario: Фильтр в адресе #### Scenario: Фильтр в адресе
@ -130,17 +161,25 @@ SHALL проверять агента (`GET /agent`; офлайн — преду
- **WHEN** вторая порция вернула 17 задач - **WHEN** вторая порция вернула 17 задач
- **THEN** в таблице 67 задач, «Показать ещё» не показана - **THEN** в таблице 67 задач, «Показать ещё» не показана
#### Scenario: Фильтр по закрытому спринту
- **WHEN** открыт адрес `/app/issues?sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет
- **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру
### Requirement: Карточка задачи ### Requirement: Карточка задачи
Страница `/app/issues/{key}` SHALL показывать задачу из `GET /issues/{key}`: поля зеркала, ссылку на задачу в Jira, Страница `/app/issues/{key}` SHALL показывать задачу из `GET /issues/{key}`: поля зеркала, ссылку на задачу в Jira,
описание как текст с сохранёнными переносами строк (без разбора разметки Jira), исходящие и входящие связи с подписью описание как текст с сохранёнными переносами строк (без разбора разметки Jira), исходящие и входящие связи с подписью
типа (логический тип из `link_types`, иначе имя типа Jira) и ссылками на карточки связанных задач, 10 последних команд типа (логический тип из `link_types`, иначе имя типа Jira) и ссылками на карточки связанных задач, 10 последних команд
по задаче (id, пакет, тип, статус, время завершения, ошибка). Если задачи нет в зеркале (404), страница SHALL сообщать по задаче (id, пакет, тип, статус, время завершения, ошибка), а если команд не было — текст «команд по задаче не было». Если задачи нет в зеркале (404), страница SHALL сообщать
об этом со ссылкой на список. об этом со ссылкой на список.
#### Scenario: Связанная задача #### Scenario: Связанная задача
- **WHEN** у AURORA-1620 исходящая связь `ASUPR_Блокер_Начала` на AURORA-1621 - **WHEN** у AURORA-1620 исходящая связь `ASUPR_Блокер_Начала` на AURORA-1621
- **THEN** в карточке «блокирует начало → AURORA-1621», ключ ведёт на `/app/issues/AURORA-1621` - **THEN** в карточке «блокирует начало → AURORA-1621», ключ ведёт на `/app/issues/AURORA-1621`
#### Scenario: Задача без команд
- **WHEN** по AURORA-1620 не было команд
- **THEN** в разделе «Последние команды» — текст «команд по задаче не было», таблицы нет
### Requirement: Служебные заметки ### Requirement: Служебные заметки
Карточка задачи SHALL давать править служебные заметки: «вид работы» — выбор из значений `kind_labels` настроек Карточка задачи SHALL давать править служебные заметки: «вид работы» — выбор из значений `kind_labels` настроек
(и текущего значения, если его там нет, и пустого), «статус по факту» — текст с подсказками из ключей `status_labels`, (и текущего значения, если его там нет, и пустого), «статус по факту» — текст с подсказками из ключей `status_labels`,
@ -161,10 +200,15 @@ SHALL проверять агента (`GET /agent`; офлайн — преду
### Requirement: Сборка и разработка ### Requirement: Сборка и разработка
Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и
`package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в `package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в
`src/jira_hub/web/app/`; каталог сборки не хранится в git. `make check` SHALL запускать `uv run pytest`, проверку типов `src/jira_hub/web/app/`; каталог сборки не хранится в git. Шрифты и прочие ресурсы
страницы SHALL входить в сборку: приложение не обращается к внешним адресам. `make check` SHALL запускать `uv run pytest`, проверку типов
и линтер фронта и тесты фронта; зелёным считается только весь набор. `npm run dev` SHALL поднимать сервер разработки с и линтер фронта и тесты фронта; зелёным считается только весь набор. `npm run dev` SHALL поднимать сервер разработки с
прокси маршрутов API на хаб из `JIRA_HUB_URL` (по умолчанию тестовый `http://127.0.0.1:8791`). прокси маршрутов API на хаб из `JIRA_HUB_URL` (по умолчанию тестовый `http://127.0.0.1:8791`).
#### Scenario: Сборка с нуля #### Scenario: Сборка с нуля
- **WHEN** в чистом checkout выполнен `make web` - **WHEN** в чистом checkout выполнен `make web`
- **THEN** в `src/jira_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки - **THEN** в `src/jira_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки
#### Scenario: Без интернета
- **WHEN** приложение открыто на машине без доступа в интернет
- **THEN** страница не запрашивает ресурсов вне хаба, текст набран IBM Plex Sans