docs(openspec): change web-app — веб-приложение хаба на React (#21)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3931a88ce8
commit
69a21d63b6
2
openspec/changes/web-app/.openspec.yaml
Normal file
2
openspec/changes/web-app/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-14
|
||||
186
openspec/changes/web-app/design.md
Normal file
186
openspec/changes/web-app/design.md
Normal file
@ -0,0 +1,186 @@
|
||||
## Context
|
||||
|
||||
Хаб (`main` 3931a88) отдаёт по `GET /` и `GET /links` страницу доски связей `src/jira_hub/web/links.html` (~1200 строк,
|
||||
из них ~900 — JS без зависимостей). Разделы скрипта: справочники и константы (`EPICS`, `KIND_LABELS`, `KIND`, `PRIO`,
|
||||
`LINK_TYPES`, геометрия), состояние и хранилище (`MODEL`, фильтры `S`, выборка `VIS`, раскладка `rel/pos`, `localStorage`
|
||||
`graph-<имя>…` с `migrateStore`), разбор (`fromJiraCsv`, `fromJson`, `fromHub`), выборка и анализ (`computeVisible`,
|
||||
`criticalPath`, `findProblems`), раскладка и отрисовка (`render`, `relayout`, `renderLane`, `renderCard`, `startDrag`,
|
||||
`alignEdges`, `drawEdges`, `orthoPath`, колонки дат), панель (`makeMulti`, легенда, проблемы, таблица), хаб
|
||||
(`hubFetch`, `loadHub`, `refreshFromJira`, `watchBatch`, `resumeWatch`), экспорт/импорт JSON (`arm-ka-links/1`).
|
||||
Финальное ревью `viewer-live` отложило M-1…M-7 (jira-hub#20): гонки поздних ответов и опроса, бесконечный опрос,
|
||||
перезаписываемые сообщения, форма входа, непокрытая ветка `mirror.board(own=False)`.
|
||||
|
||||
API хаба для интерфейсов уже есть и не меняется: `GET /board` (сырой снимок зеркала), `GET /issues` (фильтры `sprint`,
|
||||
`assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`; `limit`/`offset`, без общего
|
||||
количества), `GET /issues/{key}` (поля, `links_out`, `links_in`, `note`, 10 последних `commands`), `PUT /issues/{key}/note`
|
||||
(поля `fact_status`, `work_kind`, `done_text`, `rest_text`, `owner_note`; 422 на неизвестные), `GET /settings`
|
||||
(без токенов; `kind_labels`, `link_types`, `status_labels`), `GET /agent`, `POST /refresh`, `GET /batches/{id}`.
|
||||
HTTP — `http.server`, маршруты `ROUTES` с областью токена; не-JSON ответ — `Raw(content_type, bytes)`.
|
||||
|
||||
На Mac mini стоят Node 24 и npm 11; хаб работает под launchd (`uv run --project <основной checkout> jira-hub serve`).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** веб-приложение хаба, в которое доска перенесена без потери поведения и раскладок; страница задач зеркала
|
||||
с карточкой и правкой заметок; закрыть #20; код фронта, который владелец может читать, а агент — развивать (`viewer-edit`).
|
||||
|
||||
**Non-Goals:** запись в Jira из интерфейса, страницы очереди/агента/настроек, изменения API, изменения логики доски
|
||||
(кроме «Открыть карточку»), доступ из сети.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Отдельный npm-проект `web/` в корне репозитория.** `web/package.json`, `web/vite.config.ts`, `web/tsconfig.json`,
|
||||
исходники `web/src/`, тесты рядом с кодом (`*.test.ts(x)`), фикстуры `web/src/**/__fixtures__/`. Python-пакет не
|
||||
меняется. Менеджер пакетов — npm, `package-lock.json` в git, установка `npm ci`.
|
||||
2. **Стек.** React 19, TypeScript (strict), Vite, React Router (`createBrowserRouter`, `basename: "/app"`),
|
||||
TanStack Query (загрузка, кэш, опрос), Vitest + Testing Library (jsdom), ESLint (typescript-eslint, react-hooks),
|
||||
Playwright для e2e. Стили — обычный CSS: переменные и классы `links.html` переносятся в `web/src/styles/`, у страниц —
|
||||
свои файлы CSS. UI-кита и библиотеки графов нет. Версии в `package.json` — точные, без `^`.
|
||||
3. **Приложение под `/app/`.** Пути API (`/issues`, `/board`, …) заняты, от них зависят агент и MCP; префикс разводит
|
||||
адреса приложения и API без изменения API. Vite: `base: "/app/"`, `build.outDir: "../src/jira_hub/web/app"`,
|
||||
`emptyOutDir: true`. Маршруты приложения: `/app/board`, `/app/issues`, `/app/issues/:key`; `/app` и `/app/` →
|
||||
`/app/board`; неизвестный путь внутри приложения — страница «не найдено» со ссылками на разделы.
|
||||
4. **Раздача в хабе** (`api.py`, без области токена — в сборке нет данных):
|
||||
- `GET /app/assets/<путь>` — файл из `src/jira_hub/web/app/assets/`; путь проверяется: после `resolve()` он SHALL
|
||||
лежать внутри каталога `assets`, иначе 404; `Content-Type` по расширению (`.js` → `text/javascript; charset=utf-8`,
|
||||
`.css` → `text/css; charset=utf-8`, `.svg` → `image/svg+xml`, `.png`, `.ico`, `.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` (сборка не выполнена) — 503 `text/html` с текстом «Веб-приложение не собрано: выполните
|
||||
`make web` в каталоге jira-hub и перезапустите хаб» — на любой `/app…`;
|
||||
- `GET /` и `GET /links` — 302 `Location: /app/board`;
|
||||
- `PAGE` и `links.html` удаляются. Файлы читаются с диска на каждый запрос (как сейчас `links.html`): новая сборка
|
||||
видна без перезапуска.
|
||||
5. **Сборка не в git, выкатка с `make web`.** `src/jira_hub/web/app/` — в `.gitignore`. Новый `Makefile` в корне:
|
||||
`make web` = `cd web && npm ci && npm run build`; `make check` = `uv run pytest` + `cd web && npm run lint && npm test`.
|
||||
Выкатка: `git pull` → `make web` → `launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`. Pytest не зависит от
|
||||
сборки: тесты раздачи подкладывают свой каталог сборки (параметр `Hub`/путь, подменяемый в тесте).
|
||||
6. **Разработка.** `npm run dev` — Vite на `127.0.0.1:5173` с `base: "/app/"` и прокси всех путей API на хаб
|
||||
(`JIRA_HUB_URL`, по умолчанию `http://127.0.0.1:8791` — тестовый хаб); прокси перечисляет префиксы API явно
|
||||
(`/board`, `/issues`, `/settings`, `/agent`, `/refresh`, `/batches`, `/commands`, `/sync-runs`).
|
||||
7. **Один клиент API `web/src/api/client.ts`.** `fetch` того же хоста с `Authorization: Bearer <токен>`,
|
||||
`Accept: application/json`, `cache: "no-store"`. Результат — данные или типизированная ошибка:
|
||||
`AuthError` (401), `HubDownError` (сеть или 5xx/502/503), `ApiError {status, error, detail}` (прочие 4xx).
|
||||
Типы ответов — `web/src/api/types.ts` по спеке hub-api. Запросы — хуки TanStack Query в `web/src/api/queries.ts`
|
||||
(`useBoard`, `useSettings`, `useIssues`, `useIssue`, `useAgent`, `useBatch`) и мутации (`useRefresh`, `useSaveNote`).
|
||||
8. **Вход.** Токен — `localStorage["jira-hub-token"]` (тот же ключ, что у `links.html`) и копия в памяти на случай
|
||||
закрытого хранилища. Нет токена или любой запрос ответил `AuthError` → экран входа поверх приложения: панели и
|
||||
страницы не показываются, запросы не идут (`enabled: false`), поле токена не трогается фоновыми событиями (M-6).
|
||||
После ввода — проверка `GET /settings`; при успехе токен сохраняется, кэш запросов сбрасывается. «Сменить токен» в
|
||||
оболочке очищает токен и показывает экран входа. Загрузки файла CSV/JSON с экрана входа, как у `links.html`, нет:
|
||||
разбору нужны словари из `/settings`, а приложение отдаёт сам хаб, так что без хаба его не открыть; файл загружается
|
||||
на доске после входа.
|
||||
9. **Оболочка.** Верхняя панель: название «Хаб Jira», ссылки «Доска», «Задачи», «Сменить токен». Граница ошибок
|
||||
(`ErrorBoundary`) на уровне страницы: неожиданное исключение отрисовки → сообщение и кнопка «Перезагрузить», панель
|
||||
остаётся.
|
||||
10. **Доска: модули без React** (`web/src/board/`):
|
||||
- `constants.ts` — `EPICS` (запасной словарь имён эпиков для CSV), `KIND`, `PRIO`, `JIRA_URL`, `RELATED`, служебные
|
||||
дорожки, геометрия; `KIND_LABELS` и `LINK_TYPES` в коде не задаются — приходят параметром `dicts` из `/settings`
|
||||
(`kind_labels`, `link_types`; у `link_types` — обратный словарь «имя Jira → логический тип»);
|
||||
- `model/` — `fromHub(snap, dicts)`, `fromCsv(text, fileName, dicts)`, `fromJson(text, fileName)` → `BoardModel`;
|
||||
`util.ts` — `sortableDate`, `shortName`, `plural`, `fmtDate`, `notStarted`, `unassigned`;
|
||||
- `analysis/` — `computeVisible(model, filters)`, `criticalPath`, `findProblems`, `emptyReason`;
|
||||
- `layout/` — раскладка дорожек и карточек (`relayout`, `autoHeight` через переданную функцию измерения,
|
||||
`clampToCol`), колонки дат, `alignEdges`, `orthoPath`, `edgePath`;
|
||||
- `storage.ts` — ключи `graph-<имя>…`, `migrateStore`, чтение/запись раскладки, дорожек, фильтров, темы; все
|
||||
обращения к `localStorage` в `try/catch`, как сейчас.
|
||||
Алгоритмы переносятся построчно; переименование и упрощение — только без изменения результата на golden-фикстурах.
|
||||
11. **Доска: состояние и компоненты.** Модель и источник — в `useBoardSource` (раздел 12); фильтры, раскладка и
|
||||
переключатели — `useReducer` в `BoardPage` с сохранением в `storage.ts`. Компоненты: `Toolbar` (источник, «Перечитать»,
|
||||
«Обновить из Jira», «файл…», статистика, «Проблемы», меню «⋯» без «Сменить токен»), `FilterBar` + `MultiSelect`,
|
||||
`Legend`, `Board` (контейнер с двумя SVG: `Edges`, `Guides`), `Lane`, `Card`, `DatesBar`, `ProblemsDrawer`,
|
||||
`LinkTable`, `ExportBox`, `StatusLine`. Перетаскивание и растяжка — pointer-события на `Card`, во время движения
|
||||
положение пишется в состояние раскладки, сохранение — по `pointerup` (как сейчас). Размер карточки «по содержимому»
|
||||
измеряется через `ref` на DOM карточки.
|
||||
12. **Доска: источник данных и обновление (закрывает M-1…M-5).** `useBoardSource` хранит явный источник:
|
||||
`{kind: "hub"}` или `{kind: "file", model, fileName}`. Данные хаба (`useBoard`) показываются только при источнике
|
||||
`hub`; загрузка CSV/JSON-снимка переключает источник на `file`, поздний ответ хаба его не затирает (M-1); «Перечитать»
|
||||
возвращает источник `hub` и перезапрашивает `/board`. Импорт JSON в режиме хаба применяет раскладку, дорожки и
|
||||
фильтры и перечитывает хаб (как сейчас). «Обновить из Jira»: `GET /agent` (офлайн → предупреждение, пакет всё равно
|
||||
ставится) → мутация `POST /refresh {full: true, author: "viewer"}` → `useBatch(id)` с `refetchInterval` 3 с до
|
||||
терминального статуса пакета → `invalidate(/board)`. Кнопка выключена, пока мутация в пути или есть активный пакет
|
||||
(M-2); опрос пакета — один запрос по ключу `["batch", id]`, второго опроса не бывает. Незавершённый пакет `viewer`
|
||||
ищется (`GET /batches`) при каждой успешной загрузке `/board`, а не только при первой (M-3). Опрос останавливается
|
||||
на 404 сразу и после 5 ошибок `HubDownError` подряд — с сообщением в строке состояния и снова включённой кнопкой (M-4).
|
||||
`StatusLine` показывает одно состояние из перечня: «загрузка», «данные хаба на <время>», «из файла <имя>»,
|
||||
«пакет #N выполняется», «пакет #N выполнен», «пакет #N: ошибка <текст>», «хаб не отвечает — показаны прежние данные»,
|
||||
«агент офлайн — пакет поставлен»; ошибки разбора файла — отдельным текстом «файл не разобран: <причина>», не «хаб не
|
||||
отвечает» (M-5).
|
||||
13. **Доска: «Открыть карточку».** В заголовке карточки (не у заглушек) рядом с ключом — значок-ссылка «ⓘ» на
|
||||
`/app/issues/{key}` (переход внутри приложения). Ключ по-прежнему открывает задачу в Jira, перетаскивание и выделение
|
||||
текста не меняются.
|
||||
14. **Задачи: список `/app/issues`.** Таблица: ключ, тема, тип, статус Jira, исполнитель, спринт, срок (`planned_end`,
|
||||
иначе `duedate`), эпик (имя из снимка `/board`, иначе ключ), вид работы, статус по факту, «сосед». Фильтры — параметры
|
||||
`GET /issues`, состояние фильтров — в query-строке адреса (`useSearchParams`); поиск `q` применяется с задержкой 300 мс.
|
||||
Варианты выпадающих списков (спринты, исполнители, статусы Jira, эпики, метки, виды работы, статусы по факту) —
|
||||
из `useBoard()`, без новых маршрутов. Порции по 50 (`useInfiniteQuery`, `offset`), кнопка «Показать ещё» видна,
|
||||
пока последняя порция полная (50 строк). Ключ — ссылка на карточку.
|
||||
15. **Задачи: карточка `/app/issues/:key`.** Поля задачи, «открыть в Jira», описание — текст с `white-space: pre-wrap`
|
||||
без разбора разметки Jira; связи «исходящие»/«входящие» с подписью типа (логический тип из `link_types`, иначе имя
|
||||
Jira), ключ — ссылка на карточку; 10 последних команд: id, пакет, тип, статус, время, ошибка. 404 → «задачи нет в
|
||||
зеркале» со ссылкой на список.
|
||||
16. **Задачи: заметки.** Форма в карточке: «вид работы» — выпадающий список из значений `kind_labels` (плюс текущее
|
||||
значение, если его там нет, и «—» = пусто); «статус по факту» — поле с подсказками (`datalist`) из ключей
|
||||
`status_labels`; «сделано», «осталось», «заметка владельца» — многострочные поля. «Сохранить» активна при изменениях
|
||||
и отправляет только изменённые поля (`PUT /issues/{key}/note`, `updated_by: "web"`); пустое поле отправляется как
|
||||
`null`. Успех → обновлённая заметка в кэше карточки и списка, «сохранено <время>, <кто>». 422 → текст `detail` у
|
||||
формы, введённое не теряется. Уход со страницы с несохранёнными правками → подтверждение (`useBlocker` и
|
||||
`beforeunload`).
|
||||
17. **Golden-эталоны доски из старой страницы.** Скрипт `web/scripts/golden-from-links.mjs` (разовый, в git): читает
|
||||
`links.html` из git-ревизии 3931a88, исполняет его `<script>` в `node:vm` с минимальной заглушкой DOM, вызывает
|
||||
`fromHub`/`fromJiraCsv`/`fromJson`, `computeVisible` при наборах фильтров, `criticalPath`, `findProblems`, `orthoPath`
|
||||
и пишет `<name>.expected.json` рядом с фикстурами. Фикстуры — синтетические (`<name>.board.json`, `<name>.csv`,
|
||||
`<name>.layout.json`): эпики, соседи, заглушки «нет в зеркале», все три типа связей и неизвестный тип, несколько
|
||||
спринтов, задачи без исполнителя и срока, передачи между людьми, цепочка для критического пути. Тесты Vitest
|
||||
сравнивают выход TS-модулей с эталоном побайтно после `JSON.stringify` с сортировкой ключей.
|
||||
18. **Проверки.** Vitest: golden-модули доски, `client.ts` (401/сеть/4xx), `storage.ts` (ключи, миграция, закрытое
|
||||
хранилище), `useBoardSource` (M-1…M-5 сценариями с подменённым `fetch` и таймерами), «Задачи» (фильтры в адресе,
|
||||
«Показать ещё», заметка шлёт только изменённое, 422, подтверждение ухода). Pytest: раздача `/app…` (типы, кэш,
|
||||
`index.html` на произвольный путь, 503 без сборки, обход каталога → 404, редиректы `/`/`/links`, без токена), тест
|
||||
`mirror.board` внутри открытой транзакции (M-7). Playwright e2e на тестовом хабе `:8791`
|
||||
(`JIRA_HUB_HOME` во временном каталоге, зеркало заполнено `POST /agent/state` из golden-фикстуры): вход и неверный
|
||||
токен, доска (перетаскивание, растяжка, стрелки, «по датам», «Обновить из Jira» до конца пакета с учебным агентом,
|
||||
«Открыть карточку»), задачи (фильтр → карточка → заметка). До удаления `links.html` — скриншоты старой и новой доски
|
||||
на одной фикстуре и одной раскладке, расхождения разбираются вручную. `tsc --noEmit` и ESLint — в `npm run lint`.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Переписывание ~900 строк может изменить поведение доски незаметно] → golden-эталоны старой страницы на модулях
|
||||
логики, e2e и сверка скриншотов на отрисовке; перенос построчный, улучшения — только без изменения результата.
|
||||
- [Перетаскивание через состояние React может тормозить на ~200 карточках] → перерисовывается только двигаемая
|
||||
карточка и стрелки (`memo` на `Card`, раскладка — в отдельном контексте); если e2e покажет задержку — положение во время
|
||||
движения через `ref`/`transform`, в состояние — по `pointerup`.
|
||||
- [Сборка не в git: после `git pull` без `make web` хаб отдаёт старую сборку или 503] → 503 с явной подсказкой; шаг
|
||||
выкатки в README и Migration Plan.
|
||||
- [Зависимости npm — поверхность цепочки поставок] → точные версии, `package-lock.json`, `npm ci`; зависимостей
|
||||
времени выполнения четыре (react, react-dom, react-router, @tanstack/react-query).
|
||||
- [Словари из `/settings` пусты на чистой базе] → доска без `link_types` не покажет связей; экран показывает
|
||||
«в настройках хаба нет link_types/kind_labels» вместо пустой доски.
|
||||
- [Токен в `localStorage` доступен скриптам страницы] → сторонних скриптов нет, сборка своя; доступ только с Mac mini.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Merge → в основном checkout `git pull` → `make web` → `launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`.
|
||||
2. Открыть `http://127.0.0.1:8790/` → редирект на `/app/board`; вход уже есть (тот же ключ токена), раскладка — та же
|
||||
(те же ключи `graph-…`).
|
||||
3. Проверить: доска совпадает с прежней, «Обновить из Jira» проходит, «Задачи» → карточка → заметка сохраняется.
|
||||
4. Откат: предыдущий `main` и перезапуск — `/` снова отдаёт `links.html`; раскладки в `localStorage` не меняются
|
||||
новым приложением по формату, старая страница их читает.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Нет.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|---|
|
||||
| R-1 (design #13) | 2026-10-14 | Ответ владельца «да» на вопрос «добавить переход с карточки доски в карточку задачи или оставить на viewer-edit» понят как «добавить»; переход — отдельный значок «ⓘ», щелчок по ключу по-прежнему открывает Jira | вопрос и одобрение раздела пришли одним «да»; значок не меняет существующее поведение доски | убрать значок — одна правка компонента |
|
||||
| R-2 (design #1) | 2026-10-14 | Менеджер пакетов — npm с `package-lock.json` и `npm ci` | npm уже стоит на Mac mini, ничего доставлять не нужно | переход на pnpm — замена lock-файла и команд в Makefile |
|
||||
| R-3 (design #4) | 2026-10-14 | Файлы `assets` кэшируются навсегда (`immutable`), `index.html` — `no-cache` | имена файлов сборки содержат хэш; новая сборка видна без перезапуска и без сброса кэша | при ошибке — старый интерфейс до сброса кэша браузера |
|
||||
| R-4 (design #17) | 2026-10-14 | Фикстуры доски синтетические, не снимок живого зеркала | в зеркале данные Jira банка, в личном репозитории их держать не надо | синтетика может не покрыть редкий случай живых данных — его ловит сверка на живом хабе при выкатке |
|
||||
| R-5 (design #6) | 2026-10-14 | Прокси разработки перечисляет префиксы API явно, по умолчанию на тестовый хаб `:8791` | разработка не трогает живой хаб; явный список не перехватывает пути приложения | новый маршрут API надо дописать в прокси |
|
||||
| R-6 (design #16) | 2026-10-14 | `updated_by` заметок из приложения — `"web"`; пустое поле отправляется как `null` | у хаба один клиентский токен, пользователя он не различает; `null` очищает поле так же, как MCP | автор в истории заметок неинформативен — до появления пользователей |
|
||||
| R-7 (design #14) | 2026-10-14 | Поиск `q` в списке задач — с задержкой 300 мс | без задержки запрос на каждую букву | неудобство ввода — меняется одна константа |
|
||||
| R-8 (design #4) | 2026-10-14 | Каталог сборки читается с диска на каждый запрос, без кэша в памяти хаба | так же работает `links.html`; `make web` без перезапуска хаба | лишнее чтение файла на запрос — несущественно при одном пользователе |
|
||||
| R-9 (design #8) | 2026-10-14 | Экран входа не предлагает загрузку CSV/JSON без хаба; файл — только на доске после входа | разбор файла требует словарей из `/settings`; приложение отдаёт хаб, сценария «без хаба» нет | посмотреть CSV без токена нельзя — можно кэшировать словари в `localStorage` позже |
|
||||
53
openspec/changes/web-app/proposal.md
Normal file
53
openspec/changes/web-app/proposal.md
Normal file
@ -0,0 +1,53 @@
|
||||
## Why
|
||||
|
||||
Доска связей (`src/jira_hub/web/links.html`, change `viewer-live`) работает на живых данных хаба, но это один HTML-файл
|
||||
с ~900 строками императивного JS: каждая новая возможность (запись из доски — `viewer-edit`, другие экраны моста)
|
||||
ложится на него всё тяжелее, а гонки загрузки и опроса (jira-hub#20, M-1…M-6) вытекают из его устройства. Решение
|
||||
владельца 14.10.2026: завернуть хаб в нормальное веб-приложение на React, где доска — одна из страниц.
|
||||
|
||||
Решения владельца (brainstorming 14.10.2026):
|
||||
- React + TypeScript + Vite; статику отдаёт сам хаб; открывают пока только на Mac mini; вход — клиентский токен
|
||||
в `localStorage`, как на доске;
|
||||
- доска переписывается на React (вариант A): логика — чистые TS-модули с golden-тестами против старой страницы,
|
||||
отрисовка — React-компоненты с SVG; поведение доски не расширяется, кроме действия «Открыть карточку»;
|
||||
- в первой версии, кроме доски, — страница «Задачи»: список зеркала с фильтрами, карточка задачи, правка служебных заметок;
|
||||
- приложение — под префиксом `/app/`; `/` и `/links` перенаправляют на доску; сборка не хранится в git, выкатка —
|
||||
`git pull`, `make web`, перезапуск;
|
||||
- словари видов работ и типов связей — из `GET /settings`, не дублируются в коде.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Новый проект `web/`** (npm): приложение React + TS + Vite, сборка в `src/jira_hub/web/app/` (в `.gitignore`).
|
||||
- Оболочка: экран входа по клиентскому токену, навигация «Доска» / «Задачи», «Сменить токен».
|
||||
- Страница «Доска» (`/app/board`) — перенос `links.html` один в один; закрывает M-1…M-6 jira-hub#20 устройством
|
||||
загрузки (явный источник данных, один опрос пакета, остановка опроса на 404 и после 5 ошибок подряд, одна строка
|
||||
состояния, экран входа без панели доски); действие «Открыть карточку» у карточки доски.
|
||||
- Страница «Задачи» (`/app/issues`, `/app/issues/{key}`) — список `GET /issues` с фильтрами в адресной строке и
|
||||
порциями по 50, карточка `GET /issues/{key}` (поля, описание, связи, последние команды), правка заметок
|
||||
`PUT /issues/{key}/note`.
|
||||
- **jira-hub**: раздача приложения — `GET /app/assets/*` (файлы сборки), прочие `GET /app…` → `index.html`, 503 с
|
||||
подсказкой при отсутствии сборки, `GET /` и `GET /links` → 302 на `/app/board`; `links.html` удаляется; тест
|
||||
`mirror.board(own=False)` (M-7 #20).
|
||||
- `Makefile`: `make web` (сборка), `make check` (pytest + lint + тесты фронта).
|
||||
- Спеки: новая `web-app`; `hub-api` — MODIFIED «Процесс и доступ», ADDED «Раздача веб-приложения», REMOVED «Страница
|
||||
доски связей».
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Запись в Jira из интерфейса (спринт, срок, связи, исполнитель) — change `viewer-edit`.
|
||||
- Страницы очереди, агента и синхронизаций, настроек — позже.
|
||||
- Изменения API хаба для приложения (новые поля, общее количество в `GET /issues`, адрес Jira в настройках).
|
||||
- Изменения логики доски (граф, раскладка, критический путь, проблемы, таблица, тема), кроме «Открыть карточку».
|
||||
- Доступ со других машин сети, вход без токена, хранение раскладки в базе хаба.
|
||||
- Разбор разметки Jira в описании задачи.
|
||||
|
||||
## Impact
|
||||
|
||||
- `jira-hub`: новый каталог `web/` (npm-проект), `api.py` (маршруты `/app…`, редирект `/`/`/links`, удаление `PAGE`),
|
||||
`src/jira_hub/web/links.html` удаляется, `.gitignore`, `Makefile`, тесты API и `mirror`, README, `ARCHITECTURE.md`,
|
||||
`CLAUDE.md`.
|
||||
- Новые зависимости — только для разработки и сборки фронта (Node 24 уже есть на Mac mini); у Python-пакета
|
||||
зависимостей не прибавляется.
|
||||
- Контракт с агентом и MCP не меняется; схема базы не меняется.
|
||||
- Ключи `localStorage` (`jira-hub-token`, `graph-<имя>…`) сохраняются: вход и раскладки переживают переход.
|
||||
- Issue: aep/jira-hub#21; закрывает jira-hub#20.
|
||||
66
openspec/changes/web-app/specs/hub-api/spec.md
Normal file
66
openspec/changes/web-app/specs/hub-api/spec.md
Normal file
@ -0,0 +1,66 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Процесс и доступ
|
||||
`jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:<port>` (по умолчанию 8790), база и настройки в
|
||||
`~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты API SHALL требовать
|
||||
`Authorization: Bearer <token>`: агентский токен открывает только `/agent/*`, клиентский — всё остальное. Исключение —
|
||||
раздача веб-приложения (`GET /app`, `GET /app/…`) и перенаправления `GET /`, `GET /links`: это статические файлы без
|
||||
данных, они отдаются без токена. Ответы API — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или
|
||||
`Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки
|
||||
дополнительно отвечает 502. Методы, кроме GET, POST и PUT, отвечают 501 стандартной страницей `http.server`, не JSON.
|
||||
|
||||
#### Scenario: Чужой токен
|
||||
- **WHEN** агентским токеном вызван `GET /issues`
|
||||
- **THEN** 401
|
||||
|
||||
#### Scenario: Неподдерживаемый метод
|
||||
- **WHEN** `DELETE /issues/AURORA-1`
|
||||
- **THEN** 501, тело не JSON; база не меняется
|
||||
|
||||
#### Scenario: Страница без токена
|
||||
- **WHEN** `GET /app/board` без заголовка `Authorization`, сборка приложения есть
|
||||
- **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — `index.html` сборки; данных зеркала в нём нет
|
||||
|
||||
## ADDED 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`
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Страница доски связей
|
||||
**Reason**: страница `links.html` заменена страницей «Доска» веб-приложения (спека `web-app`, требование «Доска связей»);
|
||||
поведение перенесено без изменений, кроме закрытых недочётов jira-hub#20 и действия «Открыть карточку».
|
||||
**Migration**: `GET /` и `GET /links` перенаправляют на `/app/board`; токен (`jira-hub-token`) и раскладки
|
||||
(`graph-<имя>…`) в `localStorage` читаются приложением под теми же ключами.
|
||||
163
openspec/changes/web-app/specs/web-app/spec.md
Normal file
163
openspec/changes/web-app/specs/web-app/spec.md
Normal file
@ -0,0 +1,163 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Вход по токену
|
||||
Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в `localStorage` под ключом
|
||||
`jira-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса.
|
||||
Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с
|
||||
подсказкой `uv run jira-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода.
|
||||
Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен»
|
||||
SHALL очищать токен и показывать экран входа.
|
||||
|
||||
#### Scenario: Первый вход
|
||||
- **WHEN** приложение открыто впервые, токена нет
|
||||
- **THEN** показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён
|
||||
|
||||
#### Scenario: Устаревший токен
|
||||
- **WHEN** сохранённый токен не подходит и запрос ответил 401
|
||||
- **THEN** показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны
|
||||
|
||||
#### Scenario: Токен от прежней доски
|
||||
- **WHEN** в `localStorage` есть `jira-hub-token`, сохранённый страницей `links.html`
|
||||
- **THEN** приложение входит с ним без запроса токена
|
||||
|
||||
### Requirement: Оболочка и адреса
|
||||
Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
|
||||
задач) и `/app/issues/{key}` (карточка задачи); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
|
||||
на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать переходы «Доска», «Задачи» и действие
|
||||
«Сменить токен». Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не
|
||||
пустым экраном.
|
||||
|
||||
#### Scenario: Переход между разделами
|
||||
- **WHEN** на доске выбран раздел «Задачи»
|
||||
- **THEN** адрес — `/app/issues`, показан список задач; «назад» возвращает на доску
|
||||
|
||||
### Requirement: Ошибки хаба
|
||||
Приложение SHALL различать ошибки запросов к хабу: 401 — экран входа; хаб не отвечает (сеть, 5xx) — сообщение
|
||||
«хаб не отвечает» с возможностью повторить, ранее загруженные данные остаются на экране; прочие 4xx — текст `detail`
|
||||
ответа рядом с действием, которое его вызвало.
|
||||
|
||||
#### Scenario: Хаб остановлен
|
||||
- **WHEN** доска показана, хаб остановлен, нажато «Перечитать»
|
||||
- **THEN** на доске прежние данные и сообщение «хаб не отвечает — показаны прежние данные»
|
||||
|
||||
### Requirement: Справочники из настроек хаба
|
||||
Приложение SHALL брать перевод меток видов работ (`kind_labels`) и типов связей Jira (`link_types`) из `GET /settings`
|
||||
и не содержать собственных копий этих словарей. Если нужного словаря в настройках нет, страница SHALL сообщать, какого
|
||||
словаря не хватает, вместо пустой доски или списка без подписей.
|
||||
|
||||
#### Scenario: Новый тип связи в настройках
|
||||
- **WHEN** в `link_types` настроек добавлен тип и в зеркале есть связь этого типа
|
||||
- **THEN** после перезагрузки страницы доска показывает связь этого типа без изменения кода приложения
|
||||
|
||||
### Requirement: Доска связей
|
||||
Страница `/app/board` SHALL воспроизводить поведение доски связей хаба (`links.html` ревизии 3931a88) без изменений,
|
||||
кроме перечисленных в этом требовании и в требовании «Источник данных доски»: построение модели из `GET /board`, CSV
|
||||
фильтра Jira и JSON доски (`arm-ka-links/1`); дорожки по эпикам со свёрткой и перестановкой, служебные дорожки внизу;
|
||||
карточки с перетаскиванием, растяжкой, размером по содержимому и привязкой к сетке; стрелки связей с выравниванием;
|
||||
фильтры по спринтам и исполнителям; легенда со скрытием видов работ и типов связей; «+ соседи по связям», «⇄ передачи»,
|
||||
«скрыть без связей», «⚑ критический путь», «▥ по датам» с колонками дат и их растяжкой; ящик «Проблемы»; таблица
|
||||
связей; экспорт и импорт JSON; тема; «Сбросить раскладку». Раскладка, дорожки, фильтры и тема SHALL храниться в
|
||||
`localStorage` под теми же ключами `graph-<имя>…`, что у `links.html`, с той же миграцией старых ключей.
|
||||
Результат построения модели, видимой выборки, критического пути и проблем SHALL совпадать с результатом `links.html`
|
||||
на одних и тех же данных и фильтрах. У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку»,
|
||||
ведущий на `/app/issues/{key}`; ключ карточки по-прежнему открывает задачу в Jira. Действия «Сменить токен» в меню
|
||||
доски нет — оно в верхней панели приложения. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет.
|
||||
|
||||
#### Scenario: Раскладка переживает переход
|
||||
- **WHEN** раскладка карточек сохранена страницей `links.html`, открыта `/app/board` с теми же данными
|
||||
- **THEN** карточки стоят на тех же местах, дорожки в том же порядке и свёртке, фильтры те же
|
||||
|
||||
#### Scenario: Совпадение со старой доской
|
||||
- **WHEN** на одной фикстуре `GET /board` и одном наборе фильтров построены модель, видимая выборка, критический путь
|
||||
и проблемы старой страницей и приложением
|
||||
- **THEN** результаты совпадают
|
||||
|
||||
#### Scenario: Открыть карточку
|
||||
- **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку»
|
||||
- **THEN** открыта `/app/issues/AURORA-1620` внутри приложения
|
||||
|
||||
### 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
|
||||
подхватываться при каждой успешной загрузке из хаба. Опрос пакета SHALL прекращаться при ответе 404 и после 5 ошибок
|
||||
«хаб не отвечает» подряд, с сообщением и снова доступной кнопкой. Строка состояния доски SHALL показывать одно
|
||||
текущее состояние (загрузка; данные хаба на время последнего полного прогона; из файла; пакет выполняется / выполнен /
|
||||
ошибка с текстом; хаб не отвечает — прежние данные; агент офлайн — пакет поставлен), а ошибка разбора файла — отдельным
|
||||
текстом «файл не разобран: <причина>».
|
||||
|
||||
#### Scenario: Поздний ответ хаба не затирает файл
|
||||
- **WHEN** запрос `GET /board` в пути, и в это время загружен CSV
|
||||
- **THEN** после ответа хаба на доске по-прежнему данные CSV с пометкой «из файла»
|
||||
|
||||
#### Scenario: Нет второго полного прогона
|
||||
- **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, доска перерисована (фильтр, «Перечитать»)
|
||||
- **THEN** кнопка остаётся недоступной, в хабе один пакет, его опрашивает один опрос
|
||||
|
||||
#### Scenario: Пакет пропал
|
||||
- **WHEN** опрос пакета получил 404
|
||||
- **THEN** опрос остановлен, показано сообщение, кнопка «Обновить из Jira» снова доступна
|
||||
|
||||
#### Scenario: Пакет после «Перечитать»
|
||||
- **WHEN** пакет `viewer` поставлен с другой вкладки и не завершён, нажато «Перечитать»
|
||||
- **THEN** доска показывает «пакет #N выполняется» и по завершении перечитывает данные
|
||||
|
||||
### Requirement: Список задач
|
||||
Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (ссылка на карточку), тема,
|
||||
тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы,
|
||||
статус по факту, признак «сосед». Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид
|
||||
работы, статус по факту, статус Jira, метка, эпик, только соседи, поиск по ключу или теме), храниться в query-строке
|
||||
адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`. Задачи SHALL
|
||||
загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя порция полная.
|
||||
|
||||
#### Scenario: Фильтр в адресе
|
||||
- **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель
|
||||
- **THEN** адрес содержит `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого
|
||||
адреса в новой вкладке показывает ту же выборку
|
||||
|
||||
#### Scenario: Конец списка
|
||||
- **WHEN** вторая порция вернула 17 задач
|
||||
- **THEN** в таблице 67 задач, «Показать ещё» не показана
|
||||
|
||||
### 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`
|
||||
|
||||
### Requirement: Служебные заметки
|
||||
Карточка задачи SHALL давать править служебные заметки: «вид работы» — выбор из значений `kind_labels` настроек
|
||||
(и текущего значения, если его там нет, и пустого), «статус по факту» — текст с подсказками из ключей `status_labels`,
|
||||
«сделано», «осталось», «заметка владельца» — многострочный текст. Сохранение SHALL отправлять
|
||||
`PUT /issues/{key}/note` только с изменёнными полями и `updated_by: "web"`; очищенное поле отправляется как `null`.
|
||||
После успеха SHALL показываться сохранённая заметка с временем и автором; при 422 — текст ошибки у формы без потери
|
||||
введённого. Уход со страницы с несохранёнными правками SHALL требовать подтверждения. Заметки пишутся только в хаб,
|
||||
в Jira приложение ничего не отправляет.
|
||||
|
||||
#### Scenario: Только изменённые поля
|
||||
- **WHEN** в заметке изменено только «осталось» и нажато «Сохранить»
|
||||
- **THEN** тело `PUT /issues/{key}/note` — `{rest_text: "…", updated_by: "web"}`
|
||||
|
||||
#### Scenario: Уход с несохранёнными правками
|
||||
- **WHEN** в форме заметки есть несохранённая правка и выбран раздел «Доска»
|
||||
- **THEN** приложение спрашивает подтверждение; при отказе остаётся в карточке с введённым текстом
|
||||
|
||||
### Requirement: Сборка и разработка
|
||||
Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и
|
||||
`package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в
|
||||
`src/jira_hub/web/app/`; каталог сборки не хранится в git. `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` не показывает файлов сборки
|
||||
Loading…
x
Reference in New Issue
Block a user