docs(openspec): web-app-followups — proposal, design, дельты web-app и hub-api (#29)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d38fac83fb
commit
14e6e7fcf3
2
openspec/changes/web-app-followups/.openspec.yaml
Normal file
2
openspec/changes/web-app-followups/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-15
|
||||
141
openspec/changes/web-app-followups/design.md
Normal file
141
openspec/changes/web-app-followups/design.md
Normal file
@ -0,0 +1,141 @@
|
||||
## Context
|
||||
|
||||
`main` d38fac8: веб-приложение хаба (`web/`, React 19 + TS + Vite, TanStack Query, React Router) под `/app/`, спека
|
||||
`openspec/specs/web-app/`, хаб раздаёт сборку (`h_app` в `api.py`). Отложенные находки change `web-app` собраны в
|
||||
jira-hub#29 (46 пунктов, маркеры сохраняются из PR #27). Устройство, на которое опираются правки:
|
||||
|
||||
- `api/client.ts` — единственный HTTP-клиент: `call()` берёт токен через `getToken()`, 401 → `AuthError`, сеть и 5xx →
|
||||
`HubDownError`, прочие 4xx → `ApiError {status, error, detail}`. Токен очищается `clearToken("bad")` (из
|
||||
`useBoardSource.report` и глобального обработчика ошибок `queryClient`), `AuthGate` показывает `LoginScreen`.
|
||||
- `board/useBoardSource.ts` — источник доски: `source` (hub | file), `event` (одна строка состояния), `active`
|
||||
(опрашиваемый пакет); подхват незавершённого пакета `viewer` по каждому `board.dataUpdatedAt` через `GET /batches`.
|
||||
Состояние локально для `BoardPage` и пропадает при уходе со страницы; снимок хаба — в кэше TanStack Query,
|
||||
раскладка и фильтры — в `localStorage`.
|
||||
- `GET /batches` отдаёт пакеты без команд (`queue.list_batches`); `GET /batches/{id}` — с `commands[]` (`type`).
|
||||
- `issues/IssuesPage.tsx` — фильтры в query-строке, варианты — из `filterOptions(board, settings)`.
|
||||
- `index.html` подключает IBM Plex Sans/Mono с fonts.googleapis.com; `npm audit` — 1 high в `source-map-js@1.2.1`
|
||||
(транзитивная dev-зависимость), исправление — 1.2.2 (2026-09-30).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** закрыть jira-hub#29 целиком: каждый пункт — `fixed <commit>` или `rejected <причина>`; поведение доски на тех
|
||||
же данных совпадает с текущим, кроме пунктов группы A; `make check` и e2e зелёные, e2e устойчив к повторам.
|
||||
|
||||
**Non-Goals:** см. proposal.md.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Прокрутка доски (M-3).** При размонтировании `BoardPage` запоминает `window.scrollX/scrollY` (и `scrollLeft`
|
||||
горизонтальной обёртки холста, если она прокручивается сама) в переменной модуля; при монтировании, после первой
|
||||
отрисовки холста с данными, восстанавливает. Перезагрузка страницы открывает доску сверху, как сейчас. Состояние
|
||||
источника не поднимается выше страницы — rejected часть M-3 (решение владельца).
|
||||
2. **Строка состояния при идущем пакете (T10.3).** `reload()` и `loadFile()` не сбрасывают событие `batch`, пока
|
||||
`active !== null`; сообщения о файле (`file-error`, `json-applied`) по-прежнему заменяют его (одно событие, M-5 #20), а
|
||||
следующий ответ опроса возвращает строку пакета.
|
||||
3. **Ошибка хаба при снимке из файла (T10.4).** Событие `hub-down` получает признак `fromFile`; текст — «хаб не отвечает —
|
||||
на доске снимок из файла». `hasData` при JSON-снимке без данных хаба — `true` только если в модели есть задачи.
|
||||
4. **Подхват пакета (T10.6, T10.1).** Кандидат из `GET /batches` (автор `viewer`, `queued`/`running`) проверяется запросом
|
||||
`GET /batches/{id}`: подхватывается, только если первая команда — `refresh`. Подхват не запускается по обновлению
|
||||
снимка, которое вызвал сам `finish()` (флаг «снимок после своего пакета»), — без лишнего `GET /batches` (T10.1).
|
||||
5. **Токен в ошибке (T3.5).** `call()` кладёт в `AuthError` токен, с которым ушёл запрос; `clearToken` вызывается только
|
||||
если этот токен равен текущему. Запоздавший 401 от прежнего токена игнорируется.
|
||||
6. **Вход и 4xx (T3.2).** `LoginScreen`: `ApiError` → «хаб отверг запрос: <detail или error>»; `HubDownError` — прежний
|
||||
текст «хаб не отвечает».
|
||||
7. **Фильтр вне вариантов (T13.1).** Если значение фильтра из адреса не найдено в `opts[name]`, оно добавляется первым
|
||||
вариантом с подписью «<значение> (нет на доске)» — как текущее значение «вида работы» в заметке.
|
||||
8. **Карточка задачи (O-10, O-11).** Ссылки страниц задач — `color: var(--accent)` из темы (как в списке); пустой
|
||||
`commands` — строка «команд по задаче не было» вместо пустой таблицы.
|
||||
9. **NUL в пути (T1.1).** `h_app` отвечает 404 `not_found`, если в декодированном пути есть `\x00` или разрешение пути
|
||||
падает с `ValueError`/`OSError`; остальное поведение `h_app` не меняется.
|
||||
10. **Шрифты (T2.2).** `@fontsource/ibm-plex-sans` (400, 500, 600) и `@fontsource/ibm-plex-mono` (500), импорт CSS в
|
||||
`main.tsx`; ссылка на Google Fonts из `index.html` удаляется. Файлы `woff2` попадают в `assets/`, хаб уже отдаёт
|
||||
`font/woff2`. Версии — точные, `npm install --before=2026-10-01 --save-exact`.
|
||||
11. **source-map-js (O-1).** `overrides: {"source-map-js": "1.2.2"}` в `package.json`, проверка `npm audit` (0 high)
|
||||
и сборка; если 1.2.2 не закрывает уязвимость — O-1 rejected с текстом аудита.
|
||||
12. **Эталоны (T5.1).** В фикстуру `hub-basic` добавляются задачи с длинной темой и узкие/низкие сохранённые раскладки, чтобы
|
||||
`layout.clamp` давал не только «unset»; эталоны перегенерируются `npm run golden` из `links.html@3931a88`.
|
||||
Остальные эталоны не меняются.
|
||||
13. **e2e (T15.4, T15.6).** Порт тестового хаба — одна константа в `web/e2e/hub.ts` и `playwright.config.ts` берёт её же;
|
||||
`scripts/test-hub.sh` перед стартом проверяет, что порт свободен, и выходит с понятной ошибкой. Готовность — e2e
|
||||
проходит 10 раз подряд.
|
||||
14. **Состав плана.** 8 задач: хаб; клиент и вход; источник доски; холст и страница доски; задачи и карточка; модель,
|
||||
хранилище, эталоны; окружение и e2e; документация. Исполнение — SDD-контроллер (sonnet), задачи с готовым кодом —
|
||||
haiku, интеграция — sonnet, финальное ревью ветки — opus.
|
||||
|
||||
## Пункты #29 и их судьба
|
||||
|
||||
| Маркер | Что делаем | Задача плана |
|
||||
|---|---|---|
|
||||
| T1.1 | 404 на NUL в пути (решение 9) | хаб |
|
||||
| T1.4 | тест Content-Type для svg/png/ico/woff2/map | хаб |
|
||||
| T16.1 | убрать дубль `import urllib.*` | хаб |
|
||||
| T3.2 | 4xx на входе — текст ошибки хаба (решение 6) | клиент и вход |
|
||||
| T3.3 | содержательный тест 401 и тесты `queryClient`/`queries` | клиент и вход |
|
||||
| T3.4 | `token.test` проверяет `removeItem` при закрытом хранилище | клиент и вход |
|
||||
| T3.5 | токен в `AuthError` (решение 5) | клиент и вход |
|
||||
| T3.6 | интеграционный тест AuthError → LoginScreen | клиент и вход |
|
||||
| T10.1 | без лишнего `GET /batches` (решение 4) | источник доски |
|
||||
| T10.3 | строка пакета не стирается (решение 2) | источник доски |
|
||||
| T10.4 | «на доске снимок из файла» (решение 3) | источник доски |
|
||||
| T10.5 | тест «поздний ответ» проверяет, что ответ хаба пришёл после файла | источник доски |
|
||||
| T10.6 | подхват только `refresh` (решение 4) | источник доски |
|
||||
| M-3 | прокрутка (решение 1); хранение источника — rejected | холст и страница доски |
|
||||
| T11.1 | стабильные пропсы `Card`, `memo` срабатывает | холст и страница доски |
|
||||
| T11.2 | отмена `requestAnimationFrame` в `DatesBar` | холст и страница доски |
|
||||
| T11.3 | тест перетаскивания дорожки | холст и страница доски |
|
||||
| T11.4 | точные проверки в тестах I-2/I-3 | холст и страница доски |
|
||||
| T12.1 | `onOpen()` вне updater `setOpen` | холст и страница доски |
|
||||
| T12.2 | `writeFlag`/`writeHidden` вне updater | холст и страница доски |
|
||||
| T12.3 | зависимости `useLayoutEffect`, сброс `--head-h` | холст и страница доски |
|
||||
| T12.4 | MultiSelect: `role="group"` с флажками вместо `listbox` без `option` | холст и страница доски |
|
||||
| T13.1 | значение фильтра вне вариантов (решение 7) | задачи и карточка |
|
||||
| O-10 | ссылки цветом темы (решение 8) | задачи и карточка |
|
||||
| O-11 | «команд по задаче не было» (решение 8) | задачи и карточка |
|
||||
| T14.2 | ожидания через `waitFor`/`expect.poll` вместо пауз 20 мс | задачи и карточка |
|
||||
| T14.3 | уникальный id `datalist` (`useId`) | задачи и карточка |
|
||||
| T2.1 | `canon.ts` — сравнение по кодам символов | модель, хранилище, эталоны |
|
||||
| T5.1 | фикстуры для `layout.clamp` (решение 12) | модель, хранилище, эталоны |
|
||||
| T6.3 | прямые тесты `util.ts` | модель, хранилище, эталоны |
|
||||
| T7.1 | `initialWho` считает `whoItems` один раз | модель, хранилище, эталоны |
|
||||
| T9.1 | `colWidths()` пропускает только конечные числа | модель, хранилище, эталоны |
|
||||
| T9.3 | тесты `toggleLane`/`moveLane`/`resetLayout`/`exportFileName`/`mode=all`, закрытое хранилище | модель, хранилище, эталоны |
|
||||
| T9.4 | порядок импортов `fromJson.ts` | модель, хранилище, эталоны |
|
||||
| T2.2 | шрифты из `@fontsource` (решение 10) | окружение и e2e |
|
||||
| O-1 | `overrides` source-map-js (решение 11) | окружение и e2e |
|
||||
| T4.1 | тест «Задачи и обратно» с обратным переходом | окружение и e2e |
|
||||
| T4.2 | тест ссылок страницы «не найдено» | окружение и e2e |
|
||||
| T4.3 | убрать `App.Layout`, тест берёт `Layout` | окружение и e2e |
|
||||
| T4.4 | один импорт из `@testing-library/react` | окружение и e2e |
|
||||
| T4.5 | очистка атрибутов в `theme.test` | окружение и e2e |
|
||||
| T15.1 | высота карточек в geometry сверяется с эталоном, не с формулой | окружение и e2e |
|
||||
| T15.2 | растяжка через `expect.poll`, без литерала 240 | окружение и e2e |
|
||||
| T15.4 | порт — одна константа (решение 13) | окружение и e2e |
|
||||
| T15.6 | проверка свободного порта (решение 13) | окружение и e2e |
|
||||
| M-5 | rejected: хаб запускается из checkout, wheel не используется | — |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Перегенерация эталонов (T5.1) может показать расхождение порта с `links.html` в `layout.clamp` → это дефект порта,
|
||||
чинится в коде, эталон не подгоняется.
|
||||
- Стабилизация пропсов `Card` (T11.1) затрагивает перетаскивание → e2e board/geometry обязательны в задаче.
|
||||
- Восстановление прокрутки до отрисовки высот даст прыжок → восстанавливать после первой отрисовки с данными.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
Нумерация R-n — заново в пределах этой фичи.
|
||||
|
||||
- **R-1 (решение 1), 15.10.2026.** Прокрутка хранится в переменной модуля, не в `sessionStorage`: переживает переходы
|
||||
внутри вкладки, но не перезагрузку. Обоснование: владелец согласился на «помнить, пока открыта вкладка» в смысле
|
||||
переходов; перезагрузка — новая сессия доски. Цена ошибки: если нужна и после F5 — заменить на `sessionStorage`, одна строка.
|
||||
- **R-2 (решение 4), 15.10.2026.** Тип пакета определяется дополнительным `GET /batches/{id}` по кандидату, а не
|
||||
изменением `GET /batches` в хабе. Обоснование: API хаба в этом change не меняется (Non-Goals web-app). Цена: один лишний
|
||||
запрос при найденном кандидате.
|
||||
- **R-3 (решение 5), 15.10.2026.** Запоздавший 401 со старым токеном молча игнорируется. Обоснование: новый токен уже
|
||||
проверен `GET /settings`. Цена: если новый токен тоже станет неверным, экран входа появится на следующем запросе.
|
||||
- **R-4 (решение 7), 15.10.2026.** Значение фильтра вне вариантов подписывается «(нет на доске)». Цена — текст подписи.
|
||||
- **R-5 (решение 11), 15.10.2026.** O-1 закрывается `overrides`, а не обновлением прямых зависимостей. Обоснование:
|
||||
уязвимость только в транзитивной dev-зависимости; обновление vite/vitest шире задачи. Цена: `overrides` надо снять, когда
|
||||
прямые зависимости подтянут 1.2.2 сами.
|
||||
- **R-6 (решение 12), 15.10.2026.** T5.1 закрывается расширением фикстуры `hub-basic`, а не новой фикстурой. Цена:
|
||||
перегенерация большого `hub-basic.expected.json`.
|
||||
- **R-7 (M-5), 15.10.2026.** M-5 rejected — показано владельцу в сводке понимания до утверждения разделов, возражений не было.
|
||||
47
openspec/changes/web-app-followups/proposal.md
Normal file
47
openspec/changes/web-app-followups/proposal.md
Normal file
@ -0,0 +1,47 @@
|
||||
## Why
|
||||
|
||||
Change `web-app` (jira-hub#21, PR #27) слит, финальное ревью не нашло Critical и Important. 46 мелких находок
|
||||
отложены в jira-hub#29: недочёты видимого поведения доски, входа и страниц задач, внешние шрифты, уязвимость в
|
||||
dev-зависимости, неаккуратный React-код, слабые и недостающие тесты. Решение владельца 15.10.2026 — закрыть #29
|
||||
целиком одним change.
|
||||
|
||||
Решения владельца (brainstorming 15.10.2026):
|
||||
- берутся все 46 пунктов (группы A — поведение, B — окружение, C — качество кода, D — тесты);
|
||||
- M-3: при возврате с карточки задачи на доску восстанавливается прокрутка; держать в памяти снимок из файла и строку
|
||||
состояния не нужно — данные идут из хаба, файл — запасной путь (эта часть M-3 — rejected);
|
||||
- шрифты — те же IBM Plex Sans и Mono, но из сборки, без обращения в интернет;
|
||||
- M-5 (сборка в wheel) — rejected: хаб запускается из checkout (`uv run --project`), wheel не используется.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Доска**: восстановление прокрутки при возврате на `/app/board` (M-3); строка «пакет #N выполняется» не стирается
|
||||
«Перечитать» и загрузкой файла (T10.3); при показанном снимке из файла ошибка хаба — «хаб не отвечает — на доске снимок
|
||||
из файла» (T10.4); подхватывается только пакет обновления из Jira (T10.6); без лишнего `GET /batches` после
|
||||
завершения пакета (T10.1).
|
||||
- **Вход**: ответ 4xx при проверке токена — текст ошибки хаба, а не «хаб не отвечает» (T3.2); 401 на запрос со старым
|
||||
токеном после входа с новым не выбрасывает на экран входа (T3.5).
|
||||
- **Задачи**: значение фильтра из адреса, которого нет среди вариантов, показывается в списке (T13.1); ссылки карточки —
|
||||
цветом темы (O-10); пустой список команд — текстом (O-11).
|
||||
- **Хаб**: путь `/app/assets/…` с нулевым байтом — 404, а не 500 (T1.1).
|
||||
- **Сборка**: шрифты из npm-пакетов `@fontsource`, без Google Fonts (T2.2); `source-map-js` поднимается до исправленной
|
||||
версии через `overrides` (O-1).
|
||||
- **Код и тесты** без изменения поведения: группы C и D (см. design.md, таблица пунктов).
|
||||
- Спеки: `web-app` — MODIFIED «Вход по токену», «Доска связей», «Источник данных доски», «Список задач», «Карточка
|
||||
задачи», «Сборка и разработка»; `hub-api` — MODIFIED «Раздача веб-приложения».
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Новые страницы, запись в Jira (`viewer-edit`), изменения API хаба.
|
||||
- Изменения логики доски (модель, выборка, критический путь, проблемы, раскладка): эталоны `links.html@3931a88`
|
||||
остаются мерилом; меняются только фикстуры эталонов (T5.1), не поведение.
|
||||
- Хранение состояния доски (снимок файла, событие, опрос) между страницами — rejected часть M-3.
|
||||
- Включение сборки приложения в wheel (M-5).
|
||||
|
||||
## Impact
|
||||
|
||||
- `web/`: `src/auth`, `src/api`, `src/board` (useBoardSource, BoardPage, BoardCanvas, Card, DatesBar, Dropdown,
|
||||
MultiSelect, useBoardPrefs, storage, facets), `src/issues`, `src/test`, `e2e/`, `scripts/`, `index.html`,
|
||||
`package.json`/`package-lock.json` (+ `@fontsource/ibm-plex-sans`, `@fontsource/ibm-plex-mono`, `overrides`).
|
||||
- `src/jira_hub/api.py` (`h_app`), `tests/test_web_app.py`, `tests/test_api.py`.
|
||||
- Контракт с агентом, MCP, схема базы и API не меняются. Ключи `localStorage` не меняются.
|
||||
- Issue: aep/jira-hub#29.
|
||||
39
openspec/changes/web-app-followups/specs/hub-api/spec.md
Normal file
39
openspec/changes/web-app-followups/specs/hub-api/spec.md
Normal file
@ -0,0 +1,39 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Раздача веб-приложения
|
||||
Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/jira_hub/web/app/`, читая файлы
|
||||
с диска на каждый запрос:
|
||||
- `GET /app/assets/<путь>` — файл из `assets/` сборки; путь, который после разрешения выходит за `assets/`, не
|
||||
существует или содержит нулевой байт, — 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` —
|
||||
`font/woff2`, `.map` — `application/json`, прочее — `application/octet-stream`);
|
||||
`Cache-Control: public, max-age=31536000, immutable`;
|
||||
- любой другой `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` в каталоге jira-hub;
|
||||
- `GET /` и `GET /links` — 302 с `Location: /app/board`.
|
||||
Сборка приложения не хранится в git; её создаёт `make web`.
|
||||
|
||||
#### Scenario: Файл сборки
|
||||
- **WHEN** в сборке есть `assets/index-abc123.js`, запрошен `GET /app/assets/index-abc123.js`
|
||||
- **THEN** 200, `Content-Type: text/javascript; charset=utf-8`, `Cache-Control` с `immutable`, тело — содержимое файла
|
||||
|
||||
#### Scenario: Путь приложения
|
||||
- **WHEN** запрошен `GET /app/issues/AURORA-1620`
|
||||
- **THEN** 200, тело — `index.html` сборки, `Cache-Control: no-cache`
|
||||
|
||||
#### Scenario: Выход за каталог сборки
|
||||
- **WHEN** запрошен `GET /app/assets/../../api.py` (в том числе в виде `%2e%2e`)
|
||||
- **THEN** 404, содержимое файлов вне `assets/` не отдаётся
|
||||
|
||||
#### Scenario: Приложение не собрано
|
||||
- **WHEN** каталога сборки нет, запрошен `GET /app/board`
|
||||
- **THEN** 503, тело — HTML с подсказкой `make web`
|
||||
|
||||
#### Scenario: Старый адрес доски
|
||||
- **WHEN** запрошен `GET /links` или `GET /`
|
||||
- **THEN** 302, `Location: /app/board`
|
||||
|
||||
#### Scenario: Нулевой байт в пути
|
||||
- **WHEN** запрошен `GET /app/assets/%00` или `GET /app/assets/a%00.js`
|
||||
- **THEN** 404 `not_found`, а не 500
|
||||
162
openspec/changes/web-app-followups/specs/web-app/spec.md
Normal file
162
openspec/changes/web-app-followups/specs/web-app/spec.md
Normal file
@ -0,0 +1,162 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Вход по токену
|
||||
Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в `localStorage` под ключом
|
||||
`jira-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса.
|
||||
Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с
|
||||
подсказкой `uv run jira-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода.
|
||||
Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен»
|
||||
SHALL очищать токен и показывать экран входа. Ответ 401 SHALL выводить на экран входа, только если запрос ушёл с текущим токеном: запоздавший 401 на
|
||||
запрос с прежним токеном после входа с новым не учитывается. Если проверка токена получила ответ 4xx, кроме 401,
|
||||
экран входа SHALL показывать текст ошибки хаба, а не «хаб не отвечает».
|
||||
|
||||
#### Scenario: Первый вход
|
||||
- **WHEN** приложение открыто впервые, токена нет
|
||||
- **THEN** показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён
|
||||
|
||||
#### Scenario: Устаревший токен
|
||||
- **WHEN** сохранённый токен не подходит и запрос ответил 401
|
||||
- **THEN** показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны
|
||||
|
||||
#### Scenario: Токен от прежней доски
|
||||
- **WHEN** в `localStorage` есть `jira-hub-token`, сохранённый страницей `links.html`
|
||||
- **THEN** приложение входит с ним без запроса токена
|
||||
|
||||
#### Scenario: Запоздавший 401 от прежнего токена
|
||||
- **WHEN** запрос ушёл с прежним токеном, пользователь вошёл с новым, и после этого запрос ответил 401
|
||||
- **THEN** приложение остаётся на странице, новый токен сохранён
|
||||
|
||||
#### Scenario: Хаб отверг проверку токена
|
||||
- **WHEN** проверка `GET /settings` ответила 400 с `detail`
|
||||
- **THEN** экран входа показывает текст `detail`, а не «хаб не отвечает»
|
||||
|
||||
### Requirement: Доска связей
|
||||
Страница `/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 быть значок «Открыть карточку»,
|
||||
ведущий на `/app/issues/{key}`; ключ карточки по-прежнему открывает задачу в Jira. Действия «Сменить токен» в меню
|
||||
доски нет — оно в верхней панели приложения. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку
|
||||
карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке
|
||||
захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет.
|
||||
При возврате на `/app/board` с другой страницы приложения в той же вкладке доска 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** доска показана с той же прокруткой
|
||||
|
||||
### Requirement: Источник данных доски
|
||||
Доска SHALL хранить явный источник данных — хаб или файл. Пока показан снимок из файла (CSV или JSON-снимок), ответы
|
||||
хаба SHALL его не заменять; «Перечитать» SHALL возвращать источник «хаб» и загружать `GET /board` заново без обращения к
|
||||
Jira. Импорт JSON в режиме хаба SHALL применять раскладку, дорожки и фильтры и перечитывать хаб. «Обновить из Jira»
|
||||
SHALL проверять агента (`GET /agent`; офлайн — предупреждение, пакет всё равно ставится), ставить
|
||||
`POST /refresh {full: true, author: "viewer"}`, опрашивать `GET /batches/{id}` каждые 3 с одним опросом до
|
||||
терминального статуса пакета и затем перечитывать `GET /board`. Кнопка «Обновить из Jira» SHALL быть недоступна,
|
||||
пока запрос `/refresh` в пути или пакет выполняется. Незавершённый пакет `refresh` с автором `viewer` SHALL
|
||||
подхватываться при каждой успешной загрузке из хаба; пакеты `viewer` с другими командами не подхватываются. Опрос пакета SHALL прекращаться при ответе 404 и после 5 ошибок
|
||||
«хаб не отвечает» подряд, с сообщением и снова доступной кнопкой. Шапка доски SHALL показывать
|
||||
источник данных (данные хаба на время последнего полного прогона; «из файла») и одно текущее событие (загрузка; пакет
|
||||
выполняется / выполнен / ошибка с текстом; хаб не отвечает — прежние данные, а при снимке из файла — «на доске снимок из файла»; агент офлайн — пакет поставлен; ошибка
|
||||
разбора файла — «файл не разобран: <причина>»); новое событие заменяет предыдущее целиком. Пока пакет выполняется,
|
||||
«Перечитать» и загрузка файла SHALL не стирать событие пакета.
|
||||
|
||||
#### Scenario: Поздний ответ хаба не затирает файл
|
||||
- **WHEN** запрос `GET /board` в пути, и в это время загружен CSV
|
||||
- **THEN** после ответа хаба на доске по-прежнему данные CSV с пометкой «из файла»
|
||||
|
||||
#### Scenario: Нет второго полного прогона
|
||||
- **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, доска перерисована (фильтр, «Перечитать»)
|
||||
- **THEN** кнопка остаётся недоступной, в хабе один пакет, его опрашивает один опрос
|
||||
|
||||
#### Scenario: Пакет пропал
|
||||
- **WHEN** опрос пакета получил 404
|
||||
- **THEN** опрос остановлен, показано сообщение, кнопка «Обновить из Jira» снова доступна
|
||||
|
||||
#### Scenario: Пакет после «Перечитать»
|
||||
- **WHEN** пакет `viewer` поставлен с другой вкладки и не завершён, нажато «Перечитать»
|
||||
- **THEN** доска показывает «пакет #N выполняется» и по завершении перечитывает данные
|
||||
|
||||
#### Scenario: «Перечитать» во время пакета
|
||||
- **WHEN** пакет `viewer` выполняется и нажато «Перечитать»
|
||||
- **THEN** шапка по-прежнему показывает «пакет #N выполняется»
|
||||
|
||||
#### Scenario: Хаб недоступен при снимке из файла
|
||||
- **WHEN** на доске снимок из CSV, нажато «Перечитать», хаб не ответил
|
||||
- **THEN** шапка показывает «хаб не отвечает — на доске снимок из файла»
|
||||
|
||||
#### Scenario: Чужой пакет не подхватывается
|
||||
- **WHEN** незавершённый пакет автора `viewer` состоит из команд `ensure_fields`
|
||||
- **THEN** доска его не опрашивает, кнопка «Обновить из Jira» доступна
|
||||
|
||||
### Requirement: Список задач
|
||||
Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (ссылка на карточку), тема,
|
||||
тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы,
|
||||
статус по факту, признак «сосед». Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид
|
||||
работы, статус по факту, статус Jira, метка, эпик, только соседи, поиск по ключу или теме), храниться в query-строке
|
||||
адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`; значение фильтра из адреса, которого нет среди
|
||||
вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Задачи SHALL
|
||||
загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя порция полная.
|
||||
|
||||
#### Scenario: Фильтр в адресе
|
||||
- **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель
|
||||
- **THEN** адрес содержит `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого
|
||||
адреса в новой вкладке показывает ту же выборку
|
||||
|
||||
#### Scenario: Конец списка
|
||||
- **WHEN** вторая порция вернула 17 задач
|
||||
- **THEN** в таблице 67 задач, «Показать ещё» не показана
|
||||
|
||||
#### Scenario: Фильтр по закрытому спринту
|
||||
- **WHEN** открыт адрес `/app/issues?sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет
|
||||
- **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру
|
||||
|
||||
### Requirement: Карточка задачи
|
||||
Страница `/app/issues/{key}` SHALL показывать задачу из `GET /issues/{key}`: поля зеркала, ссылку на задачу в Jira,
|
||||
описание как текст с сохранёнными переносами строк (без разбора разметки Jira), исходящие и входящие связи с подписью
|
||||
типа (логический тип из `link_types`, иначе имя типа Jira) и ссылками на карточки связанных задач, 10 последних команд
|
||||
по задаче (id, пакет, тип, статус, время завершения, ошибка), а если команд не было — текст «команд по задаче не было». Если задачи нет в зеркале (404), страница SHALL сообщать
|
||||
об этом со ссылкой на список.
|
||||
|
||||
#### Scenario: Связанная задача
|
||||
- **WHEN** у AURORA-1620 исходящая связь `ASUPR_Блокер_Начала` на AURORA-1621
|
||||
- **THEN** в карточке «блокирует начало → AURORA-1621», ключ ведёт на `/app/issues/AURORA-1621`
|
||||
|
||||
#### Scenario: Задача без команд
|
||||
- **WHEN** по AURORA-1620 не было команд
|
||||
- **THEN** в разделе «Последние команды» — текст «команд по задаче не было», таблицы нет
|
||||
|
||||
### Requirement: Сборка и разработка
|
||||
Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и
|
||||
`package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в
|
||||
`src/jira_hub/web/app/`; каталог сборки не хранится в git. Шрифты и прочие ресурсы
|
||||
страницы SHALL входить в сборку: приложение не обращается к внешним адресам. `make check` SHALL запускать `uv run pytest`, проверку типов
|
||||
и линтер фронта и тесты фронта; зелёным считается только весь набор. `npm run dev` SHALL поднимать сервер разработки с
|
||||
прокси маршрутов API на хаб из `JIRA_HUB_URL` (по умолчанию тестовый `http://127.0.0.1:8791`).
|
||||
|
||||
#### Scenario: Сборка с нуля
|
||||
- **WHEN** в чистом checkout выполнен `make web`
|
||||
- **THEN** в `src/jira_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки
|
||||
|
||||
#### Scenario: Без интернета
|
||||
- **WHEN** приложение открыто на машине без доступа в интернет
|
||||
- **THEN** страница не запрашивает ресурсов вне хаба, текст набран IBM Plex Sans
|
||||
Loading…
x
Reference in New Issue
Block a user