Compare commits

...

2 Commits

Author SHA1 Message Date
Andrey Epifancev
aad35177bf План: редизайн веб-приложения (#65)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:57:47 +04:00
Andrey Epifancev
fa20059ecd Спека: редизайн веб-приложения по дизайн-системе AURA (#65)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:54:22 +04:00
5 changed files with 1018 additions and 0 deletions

View File

@ -0,0 +1,467 @@
# Редизайн веб-приложения по дизайн-системе AURA — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** веб-приложение хаба выглядит как AURA (светлая и тёмная темы), собрано из общих компонентов и токенов;
«Обновить из Jira», тема и «Сменить токен» — в верхней панели на всех страницах; фильтры доски и списка сгруппированы.
**Architecture:** слой токенов `web/src/styles/tokens.css` (AURA + токены доски) и набор компонентов `web/src/ui/`
(вид и доступность, без логики страниц). Обновление из Jira переезжает из `board/useBoardSource.ts` в оболочку
(`shell/refresh.ts`: хук, контекст, провайдер), доска оставляет себе источник и события снимка. Страницы
перекладываются на компоненты по одной; логика доски (`board/{model,analysis,layout}`, `storage.ts`) и golden-эталоны
не меняются.
**Tech Stack:** React 19 + TypeScript + Vite, TanStack Query, Vitest + Testing Library, Playwright; `@fontsource/mulish`
5.3.0, `@fontsource/ibm-plex-mono` 5.3.0.
**Spec:** `openspec/changes/redesign/` (proposal.md, design.md — решения 1–24 и значения токенов в «Context»,
specs/web-app/spec.md). Материалы системы (вне репозитория): `~/projects/jira-bridge/design/` — экспорт Figma `*.svg`
(смотреть как картинки), `AURA-xlsx-tool-handoff-2026-09-29.zip` (`src/styles/tokens.css`, `src/styles.css`,
`design-system/tokens.json` — значения для сверки).
## Global Constraints
- Цвета светлой и тёмной темы — ровно значения из design.md «Context» (канва `#f8f8f6` / `#181818`, поверхность
`#fdfdfc` / `#1f1f1f`, текст `#17171a` / `#f2f4f2`, вторичный `#818190` / `#d6dcd7`, граница `#e5e5e0` /
`rgb(232 238 233 / 10%)`, действие `#568d32` / `#69a93d`, hover `#477729` / `#83c657`, ошибка `#b42318` / `#e26c67`,
предупреждение `#d89614` / `#e8c17a`, фокус `rgb(42 122 70 / 35%)` / `rgb(116 184 255 / 55%)`).
- Вне `web/src/styles/tokens.css` в `*.css` нет литералов цвета (`#hex`, `rgb(`, `rgba(`, `hsl(`, именованных цветов);
разрешены `transparent`, `currentColor`, `inherit`, `color-mix(...)` от переменных.
- Типографика: Mulish; текст 14/20 Regular, подпись 14/20 Semibold (600), мелкий 12/16, крупный 16/24, заголовок
страницы 20/28 Bold (700). Моно — IBM Plex Mono (ключи, код, исходник).
- Геометрия: шаг 4 px; контрол 32 px (крупный 40), радиус контрола 10; кнопка — радиус 999; панель — радиус 16;
окно — радиус 20; флажок 16×16 радиус 4; переходы 180 ms ease; фокус — кольцо 2 px `--color-focus-ring` со смещением
2 px.
- Контейнер страниц, кроме доски: `max-width: 1440px`, по центру, боковой отступ 24 px (16 px уже 640 px). Доска — вся
ширина, отступ 24 px.
- Адреса, query-параметры, ключи `localStorage` (`graph-*`, `jira-hub-token`, `jira-hub-project`…), запросы к API,
тексты сообщений `statusText` — прежние; новые ключи только `jira-hub-legend` (`localStorage`) и
`jira-hub-issue-filters` (`sessionStorage`).
- Golden-эталоны `web/src/board/__fixtures__/*.expected.json` и модули `board/{model,analysis,layout}` не менять.
- Новых зависимостей, кроме `@fontsource/mulish@5.3.0` (точная версия); `@fontsource/ibm-plex-sans` удалить.
- `dangerouslySetInnerHTML`/`innerHTML` не появляются; иконки — React-SVG.
- Подписи и комментарии — русские; коммиты заканчиваются строкой `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
- Зелёным считается только весь набор: `make check` и `cd web && npm run e2e`.
## Review Focus
1. Пакет обновления, начатый на доске, при уходе на «Задачи» и обратно: один опрос, кнопка остаётся недоступной, по
завершении список и доска перечитаны (тест в Task 4 «переход между страницами во время пакета»).
2. Доска в режиме «из файла» в момент завершения пакета: файл остаётся (тест в Task 4, перенос M-1).
3. Тёмная тема в окне карточки, меню и выпадающих списках: родной `<dialog>` и `<select>` без `color-scheme` рисуются
светлыми (тест в Task 1: `color-scheme: dark` в тёмных блоках `tokens.css`; скриншоты Task 11).
4. Смена шрифта и сохранённые размеры карточек: текст не вылезает за карточку, высота по содержимому считается тем же
слоем `.measure` (e2e «растяжка за уголок и возврат размера» в Task 7 остаётся зелёным).
5. Список с выбранными фильтрами, открытый по адресу в новой вкладке: панель «Фильтры» свёрнута, но плашки и счётчик
видны (тест в Task 8 «Фильтр в адресе»).
---
### Task 1: Токены, шрифт, проверка цветов
**Files:**
- Create: `web/src/styles/tokens.css`, `web/src/styles/tokens.test.ts`
- Modify: `web/src/styles/theme.css` (удалить файл; импорт заменить на `tokens.css` в `web/src/App.tsx`),
`web/src/styles/base.css`, `web/src/styles/controls.css`, `web/src/board/board.css`, `web/src/main.tsx`,
`web/package.json`, `web/package-lock.json`
**Interfaces:**
- Produces: CSS-переменные для всех задач. Семантические (обе темы): `--color-bg-canvas`, `--color-bg-surface`,
`--color-bg-subtle`, `--color-bg-selected` (`#f4faf5` / `#272727` — фон выбранного), `--color-bg-hover`
(`#f2f2f0` / `#272727`), `--color-bg-track` (трек сегмента, `#eeeeeb` / `#272727`), `--color-text-primary`,
`--color-text-secondary`, `--color-text-placeholder`, `--color-text-disabled`, `--color-text-on-primary`,
`--color-border-default`, `--color-border-strong`, `--color-border-selected`, `--color-action-primary`,
`--color-action-primary-hover`, `--color-action-danger`, `--color-action-danger-hover`, `--color-status-success`,
`--color-status-success-bg`, `--color-status-error`, `--color-status-error-bg`, `--color-status-warning`,
`--color-status-warning-bg`, `--color-status-warning-border`, `--color-focus-ring`, `--color-overlay`
(`rgb(0 0 0 / 45%)`), `--shadow-popover`, `--shadow-dialog`. Шкалы: `--space-1…8` (4…32), `--radius-xs/sm/md/lg/xl/2xl/round`
(4/8/10/12/16/20/999), `--font-ui` (`"Mulish", system-ui, -apple-system, "Segoe UI", sans-serif`), `--font-mono`,
`--fs-caption` 12, `--fs-body` 14, `--fs-lg` 16, `--fs-section` 20, `--lh-caption` 16, `--lh-body` 20, `--lh-lg` 24,
`--lh-section` 28, `--control-h` 32, `--control-h-lg` 40, `--control-radius` 10, `--motion` `180ms ease`.
Токены доски: `--k-decision`, `--k-analytics`, `--k-dev`, `--k-ui`, `--k-test`, `--k-org`, `--p-crit`, `--p-high`,
`--p-mid`, `--p-low`, `--edge`, `--edge-strong` (= действие), `--guide` (оранжевый, прежний), `--lane-alt`.
Псевдонимы на переход (удаляются в Task 10): `--bg`→canvas, `--fg`→text-primary, `--muted`→text-secondary,
`--line`→border-default, `--card`→surface, `--panel`→surface, `--soft`→bg-hover, `--accent`→action-primary,
`--accent-fg`→text-on-primary.
- Тёмная тема — те же три блока, что сейчас в `theme.css`: `@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){…} }`
и `:root[data-theme="dark"]{…}`; в обоих `color-scheme: dark`, в светлом `:root` — `color-scheme: light`.
- [ ] **Step 1: Тест `tokens.test.ts`** (Vitest, node `fs`): (а) «вне tokens.css нет литералов цвета» — обходит все
`web/src/**/*.css`, кроме `styles/tokens.css`, ищет `/#[0-9a-fA-F]{3,8}\b|\b(rgb|rgba|hsl|hsla)\(|:\s*(white|black|red|…)\b/`
(именованные — список CSS-цветов из 10–15 самых частых достаточно: white, black, red, green, blue, gray, grey,
orange, yellow, purple) вне комментариев; падение перечисляет `файл: литерал`. (б) «обе темы задают одни имена» —
множество `--color-*` в светлом `:root` равно множеству в `:root[data-theme="dark"]` и в медиа-блоке. (в) тёмные
блоки содержат `color-scheme: dark`.
- [ ] **Step 2:** `cd web && npx vitest run src/styles/tokens.test.ts` — FAIL (нет файла, литералы в `controls.css`,
`board.css`).
- [ ] **Step 3:** написать `tokens.css` по Interfaces и design.md «Context»; палитру `--k-*` подобрать под тёплую канву
(светлая — насыщенность и светлота как у нынешних, различимы с `#568d32`; тёмная — светлее); перенести литералы
`controls.css` (`#fff`, `rgba(0,0,0,.18)`) и `board.css` (`rgba(0,0,0,.18)`) в токены (`--color-text-on-primary`,
`--shadow-popover`); удалить `theme.css`, импорт `./styles/tokens.css` в `App.tsx` первым.
- [ ] **Step 4:** шрифт: `npm install --save-exact @fontsource/mulish@5.3.0`, `npm uninstall @fontsource/ibm-plex-sans`;
в `main.tsx` — `@fontsource/mulish/400.css`, `600.css`, `700.css`, `@fontsource/ibm-plex-mono/500.css`; `base.css`:
`body{font:var(--fs-body)/var(--lh-body) var(--font-ui);background:var(--color-bg-canvas);color:var(--color-text-primary)}`,
`h1{font-size:var(--fs-section);line-height:var(--lh-section);font-weight:700}`, `:focus-visible` — кольцо по
Global Constraints, `@media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}`.
- [ ] **Step 5:** `npx vitest run` — весь набор зелёный (старые тесты не трогали разметку); `npm run lint` — чисто.
- [ ] **Step 6: Commit** `feat(web): токены AURA, шрифт Mulish, проверка цветов (#65)`
### Task 2: Иконки и базовые компоненты
**Files:**
- Create: `web/src/ui/icons.tsx`, `web/src/ui/Button.tsx`, `web/src/ui/Segmented.tsx`, `web/src/ui/ThemeSwitch.tsx`,
`web/src/ui/Badge.tsx`, `web/src/ui/Notice.tsx`, `web/src/ui/Panel.tsx`, `web/src/ui/PageHeader.tsx`,
`web/src/ui/ui.css`, `web/src/ui/index.ts`, `web/src/ui/ui.test.tsx`
- Modify: `web/src/shell/theme.ts`, `web/src/shell/theme.test.ts`
**Interfaces:**
- Consumes: токены Task 1.
- Produces (все из `web/src/ui/index.ts`):
- `Icon` — компоненты `IconRefresh, IconUpload, IconMore, IconClose, IconChevronDown, IconSearch, IconFilter,
IconSun, IconMoon, IconExternal, IconWarning, IconCheck, IconError, IconInfo, IconHandoff, IconCritical,
IconCalendar, IconNeighbors, IconLink, IconGrid, IconReset, IconLegend, IconOpen` — `(p: {size?: 16|20}) => JSX`,
`<svg aria-hidden="true" width height viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">`.
- `Button(p: {variant?: "primary"|"secondary"|"danger"|"ghost"; size?: "md"|"lg"; icon?: ReactNode; loading?: boolean} & ButtonHTMLAttributes)`
— класс `ui-btn ui-btn--<variant> ui-btn--<size>`, `type="button"` по умолчанию; `loading` — крутилка вместо
`icon`, `disabled`, `aria-busy="true"`. `aria-pressed` пробрасывается: при `true` у `secondary` — вид «выбрано»
(фон `--color-bg-selected`, граница и текст `--color-action-primary`).
- `IconButton(p: {label: string; icon: ReactNode} & ButtonHTMLAttributes)` — 32×32, радиус 10, `aria-label={label}`, `title={label}`.
- `Segmented<T extends string>(p: {label: string; value: T; options: {value: T; label: string; href?: string}[]; onChange?(v: T): void})`
— `role="radiogroup"` + `role="radio"`/`aria-checked` для кнопок; если у вариантов есть `href` — `<nav aria-label>`
со ссылками `react-router` `Link` и `aria-current="page"` у активной. Трек `--color-bg-track`, радиус 999, отступ 4;
активный — поверхность, текст `--color-action-primary`, Semibold.
- `ThemeSwitch()` — `role="switch"`, `aria-checked` = тёмная, `aria-label="Тёмная тема"`; читает/пишет через `theme.ts`.
- `Badge(p: {tone?: "neutral"|"success"|"error"|"warning"|"info"; children})` — 20 px высота, радиус 999, 12/16 Semibold.
- `Chip(p: {children; onRemove(): void; removeLabel: string})` — плашка с кнопкой ✕ (`aria-label={removeLabel}`), вид «выбрано».
- `Notice(p: {tone: "success"|"error"|"warning"|"info"; title?: ReactNode; children?; action?: ReactNode})` —
`role="alert"` для error/warning, `role="status"` для прочих; иконка в круге 20 px, заголовок 14/20 Semibold,
текст 12/16, радиус 12, граница/фон тона.
- `Panel(p: {as?: "div"|"section"; padding?: "none"|"md"|"lg"} & HTMLAttributes)` — поверхность, граница, радиус 16, отступ 16/24.
- `PageHeader(p: {title: ReactNode; subtitle?: ReactNode; actions?: ReactNode; children?: ReactNode})` — `<h1>`,
подзаголовок 14/20 вторичным, действия справа (перенос на узких).
- `theme.ts`: `THEMES`, `readTheme`, `saveTheme`, `applyTheme` прежние; новые `systemDark(): boolean`
(`matchMedia("(prefers-color-scheme: dark)")`, без `matchMedia` — `false`) и
`effectiveDark(t: Theme): boolean` (`t === "dark" || (t === "" && systemDark())`). `THEME_NAMES` удалить, если не
используется.
- [ ] **Step 1: Тесты `ui.test.tsx`:** `Button loading` — `aria-busy="true"`, недоступна, иконка не показана;
`Button aria-pressed` пробрасывается; `IconButton` — доступное имя из `label`; `Segmented` кнопками — `radiogroup`,
щелчок вызывает `onChange("b")`, у выбранного `aria-checked="true"`; `Segmented` ссылками — внутри `MemoryRouter`,
активная ссылка `aria-current="page"`; `ThemeSwitch` — без `graph-theme` и с `matchMedia` → dark показывает
`aria-checked="true"`, щелчок → `graph-theme` = `light`, `data-theme="light"`; второй щелчок → `dark`; `Chip` —
кнопка «Снять фильтр Спринт» вызывает `onRemove`; `Notice tone="error"` — `role="alert"`.
`theme.test.ts`: `effectiveDark("")` следует `matchMedia`, `effectiveDark("light") === false`.
- [ ] **Step 2:** `npx vitest run src/ui src/shell/theme.test.ts` — FAIL.
- [ ] **Step 3:** реализовать компоненты и `ui.css` (только переменные из Task 1; размеры — Global Constraints;
`Button primary` — фон действия, текст `--color-text-on-primary`, hover — `--color-action-primary-hover`;
`secondary` — прозрачный фон, граница `--color-border-strong`, текст основной, hover — граница действия;
`danger` — фон `--color-action-danger`; `ghost` — без рамки, текст действия; `lg` — 40 px, отступ 20).
- [ ] **Step 4:** `npx vitest run` и `npm run lint` — зелёные.
- [ ] **Step 5: Commit** `feat(web): иконки и базовые компоненты дизайн-системы (#65)`
### Task 3: Компоненты форм, окно, меню
**Files:**
- Create: `web/src/ui/Field.tsx`, `web/src/ui/inputs.tsx` (`TextInput`, `SearchInput`, `Select`, `Checkbox`,
`Textarea`), `web/src/ui/Dialog.tsx`, `web/src/ui/Menu.tsx`, `web/src/ui/forms.test.tsx`
- Modify: `web/src/ui/index.ts`, `web/src/ui/ui.css`, `web/src/board/components/Dropdown.tsx` (переносится в
`ui/Menu.tsx` как `Dropdown`; старый файл — реэкспорт или удаление с правкой импортов `MultiSelect`, `Toolbar`)
**Interfaces:**
- Produces:
- `Field(p: {label: string; hint?: ReactNode; error?: ReactNode; children: ReactElement})` — `<label>` с подписью
14/20 Semibold над контролом, подсказка 12/16 вторичным, ошибка — цветом ошибки с `role="alert"`.
- `TextInput`, `Textarea` — обёртки над `<input>`/`<textarea>` с классом `ui-input` (32 px, радиус 10, отступ 12,
граница `--color-border-default`, hover — `--color-border-strong`, фокус — граница действия + кольцо,
`aria-invalid="true"` — граница ошибки); `size?: "md"|"lg"`.
- `SearchInput(p: {value: string; onChange(v: string): void; label: string; placeholder?: string})` — `type="search"`,
`aria-label={label}`, иконка поиска слева, кнопка «Очистить» (`IconClose`, видна при непустом значении).
- `Select(p: SelectHTMLAttributes & {size?: "md"|"lg"})` — родной `<select>` с `appearance:none` и `IconChevronDown`
поверх справа (`pointer-events:none`).
- `Checkbox(p: {label: ReactNode} & InputHTMLAttributes)` — `<label>` + `<input type="checkbox">` 16×16, радиус 4,
`accent-color` действия.
- `Dialog(p: {label: string; onClose(): void; size?: "sm"|"md"|"lg"; className?: string; children})` — родной
`<dialog>`, `showModal()` при монтировании, `aria-label`, `onCancel` → `preventDefault` + `onClose`; ширина
sm 480 / md 640 / lg 1000 px (`min(…, 100vw - 32px)`), радиус 20, фон поверхности, `::backdrop` — `--color-overlay`;
уже 700 px — во весь экран, как сейчас у `.issue-modal`. Закрытие по фону — ответственность использующего
(`IssueModal` уже это умеет).
- `Dropdown` — прежняя сигнатура `({button, className, children, onOpen})`, классы `dd`, `dd-menu`, `menu` сохраняются;
вид — поверхность, граница, радиус 12, `--shadow-popover`, пункты 32 px радиус 8.
- `Menu(p: {label: string; children: (close: () => void) => ReactNode; align?: "start"|"end"})` — `Dropdown` с
`IconButton label={label} icon={<IconMore/>}`; пункты — `MenuItem(p: {onSelect(): void; pressed?: boolean; danger?: boolean; icon?; children})`
(`role` не меняем: кнопки, `aria-pressed` для переключателей), `MenuSeparator()`.
- [ ] **Step 1: Тесты `forms.test.tsx`:** `Field` связывает подпись с полем (`getByLabelText("Метка")`); `SearchInput`
— ввод вызывает `onChange`, «Очистить» → `onChange("")` и видна только при значении; `Select` — `getByRole("combobox",
{name})` и выбор значения; `Dialog` — открыт (`open`), Esc вызывает `onClose`; `Menu` — кнопка «Ещё» открывает
пункты, `MenuItem pressed` → `aria-pressed="true"`, выбор пункта вызывает `onSelect` и закрывает меню; Escape и
щелчок вне закрывают (как в нынешнем `Dropdown.test.tsx` — перенести его в `ui/`).
- [ ] **Step 2:** `npx vitest run src/ui` — FAIL.
- [ ] **Step 3:** реализовать; импорты `Dropdown` в `board/components` перевести на `../../ui`.
- [ ] **Step 4:** `npx vitest run` и `npm run lint` — зелёные.
- [ ] **Step 5: Commit** `feat(web): поля, выпадающие списки, окно и меню дизайн-системы (#65)`
### Task 4: Обновление из Jira — в оболочку
**Files:**
- Create: `web/src/shell/refresh.ts` (хук и контекст), `web/src/shell/RefreshProvider.tsx`,
`web/src/shell/refreshStatus.ts`, `web/src/shell/refresh.test.tsx`
- Modify: `web/src/board/useBoardSource.ts`, `web/src/board/status.ts`, `web/src/board/useBoardSource.test.tsx`,
`web/src/App.tsx` (провайдер внутри `QueryClientProvider` и `AuthGate`)
**Interfaces:**
- Produces:
- `refreshStatus.ts`: тип `RefreshEvent` = прежние пакетные виды `BoardStatus` (`batch`, `batch-done`,
`batch-failed`, `batch-cancelled`, `batch-lost`) + `{kind: "hub-down"}` + `{kind: "error"; message: string}`;
`refreshText(e: RefreshEvent): {text: string; tone: "info"|"success"|"error"|"warning"}` — тексты пакетных видов
дословно из нынешнего `statusText`; `hub-down` — «хаб не отвечает — пакет не поставлен».
- `refresh.ts`: `interface JiraRefresh { event: RefreshEvent | null; busy: boolean; agent: AgentView | null; start(): Promise<void>; track(batchId: number): void }`,
`RefreshContext`, `useJiraRefresh(): JiraRefresh` (вне провайдера — ошибка с понятным текстом).
- `RefreshProvider({children, pollMs = POLL_MS})` — владелец состояния: постановка (`GET /agent` → `POST /refresh
{full: true, author: "viewer"}`), один опрос `["batch", id]` (правила остановки — как сейчас в `useBoardSource`:
терминальный статус, `AuthError`/`ApiError`, `MAX_POLL_ERRORS` подряд), подхват незавершённого пакета `viewer` с
первой командой `refresh` при монтировании и после каждой успешной загрузки запроса с ключом `["board", …]`
(подписка `queryClient.getQueryCache().subscribe`, событие `updated` с `action.type === "success"`), кроме
перечитывания после своего пакета; `GET /agent` — `useQuery(["agent"], refetchInterval 60_000)`. Завершение:
`invalidateQueries` для `["board"]`, `["issues"]`, `["issue"]`, `["projects"]`; событие `batch-done` сбрасывается
в `null` через 10 с. `track(id)` — начать опрос известного пакета (форма «Новый проект»).
- `useBoardSource` после правки: `BoardSource` без `refreshing` и `refreshFromJira`; `status` — только события доски
(`loading`, `hub-down`, `file-error`, `json-applied`, `error`); `BoardStatus` в `board/status.ts` — без пакетных
видов, `statusText` — без них.
- [ ] **Step 1: `refresh.test.tsx`** — перенести из `useBoardSource.test.tsx` тесты M-2 (двойное нажатие — один пакет,
один опрос), M-3 (подхват после загрузки снимка), M-4 (404, 5 ошибок подряд), T10.2 (4xx — `batch-lost` с detail,
`busy` сброшен), «пакет с ошибкой команды; агент офлайн», «уход и возврат во время пакета», T10.6 (пакет без
`refresh` не подхватывается), 401 на запуске (токен сброшен), запоздавший 401 — на `RefreshProvider` через
`renderHook(useJiraRefresh, {wrapper})`. Новые: «по завершении инвалидируются board, issues, issue, projects»
(шпион на `invalidateQueries` или счётчики запросов `GET /issues` и `GET /board`); «`track(9)` опрашивает пакет 9
без `POST /refresh`»; «`batch-done` исчезает через 10 с» (фальшивые таймеры); «переход между страницами во время
пакета» — провайдер смонтирован один раз, хук доски размонтирован и смонтирован снова: `POST /refresh` один раз,
`maxInFlight("GET /batches/7") === 1`, `busy === true` до завершения.
- [ ] **Step 2:** в `useBoardSource.test.tsx` убрать перенесённые тесты; оставить M-1 (поздний ответ, «Перечитать»),
hub-down при «Перечитать», file-error, импорт JSON, T10.4 (JSON-снимок без хаба); «M-1: пакет, завершившийся при
показанном файле, не заменяет файл» переписать: провайдер + доска, пакет завершился — `source === "file"`.
«T10.4: пакет завершился при показанном CSV, хаб не ответил» остаётся тестом доски (провайдер + доска): событие
доски `hub-down` с `fromFile`. ««Обновить из Jira» при показанном CSV: хаб недоступен» переезжает в
`refresh.test.tsx`: событие оболочки `hub-down`, доска по-прежнему из файла.
- [ ] **Step 3:** `npx vitest run src/shell/refresh.test.tsx src/board/useBoardSource.test.tsx` — FAIL.
- [ ] **Step 4:** реализовать `refresh.ts`, `RefreshProvider.tsx`, `refreshStatus.ts`; вычистить пакетную часть из
`useBoardSource.ts` и `status.ts`. `Toolbar.tsx` и `BoardPage.tsx` временно берут кнопку из `useJiraRefresh()`
(кнопка «Обновить из Jira» пока на прежнем месте — уйдёт в Task 5), строка состояния доски — `statusText` доски или,
если его нет, `refreshText` пакета.
- [ ] **Step 5:** `npx vitest run` и `npm run lint` — зелёные (тесты `BoardPage.test.tsx` про «Обновить из Jira»
оборачиваются в провайдер через общий `wrapper`).
- [ ] **Step 6: Commit** `refactor(web): обновление из Jira — состояние оболочки, доска хранит только источник (#65)`
### Task 5: Верхняя панель приложения
**Files:**
- Create: `web/src/shell/AppBar.tsx`, `web/src/shell/DataStatus.tsx`, `web/src/shell/AppBar.test.tsx`
- Modify: `web/src/shell/Layout.tsx`, `web/src/shell/shell.css`, `web/src/projects/ProjectPicker.tsx`,
`web/src/projects/NewProjectDialog.tsx` (`onCreated(p: Project, batchId: number)`), `web/src/board/components/Toolbar.tsx`
(убрать «Обновить из Jira» и пункт темы), `web/src/App.test.tsx`, `web/src/projects/projects.test.tsx`,
`web/src/board/BoardPage.test.tsx`, `web/e2e/hub.ts` (`login` ждёт ссылку «Доска связей»), `web/e2e/*.spec.ts`
(подписи разделов)
**Interfaces:**
- Consumes: `useJiraRefresh` (Task 4), `Segmented`, `ThemeSwitch`, `Button`, `Menu`/`MenuItem`, `Select`, `Badge`,
иконки (Tasks 2–3); `useBoard(project)` для `last_sync`.
- Produces: `AppBar({section, project}: {section: "/board"|"/issues"; project: string | null})`;
`DataStatus({project}: {project: string | null})` — точка агента + подпись («агент на связи» / «агент офлайн»,
`title` — «последний выход на связь: ДД.ММ ЧЧ:ММ»), «данные на ДД.ММ ЧЧ:ММ» (`fmtStamp(last_sync.finished_at)`) /
«полного обновления не было», `Button primary icon={<IconRefresh/>} loading={busy}` «Обновить из Jira», текст
события `refreshText` цветом тона (`role="status"`).
- Разметка панели (design.md, решение 9): `<header class="appbar">` высота 64, поверхность, нижняя граница, отступы
24; «Хаб Jira» Bold 16/24; `ProjectPicker` на `Select` (метка `aria-label="Проект"`, варианты и «+ Новый
проект…» — прежние); `Segmented` ссылками «Доска связей» / «Задачи» (сохраняет `?project=`); растяжка;
`DataStatus`; `ThemeSwitch`; `Menu label="Ещё"` с `MenuItem` «Сменить токен». Уже 1100 px — `DataStatus` второй
строкой (`flex-wrap`, `order`); уже 640 px — название скрыто, `Segmented` на всю ширину.
- [ ] **Step 1: Тесты `AppBar.test.tsx`:** «Обновить из Jira» на `/issues` ставит `POST /refresh` и становится
недоступной (`aria-busy`), после `done` — «пакет #7 выполнен» и `GET /issues` запрошен повторно; агент офлайн —
«агент офлайн»; «данные на 14.10 12:00» из `last_sync` снимка; меню «Ещё» → «Сменить токен» показывает экран входа;
разделы — ссылки «Доска связей» и «Задачи», активная с `aria-current="page"`. `projects.test.tsx`: «Создание проекта
из формы» — верхняя панель показывает «пакет #N в очереди» без `GET /batches` списка (через `track`).
`App.test.tsx`/`BoardPage.test.tsx`: подписи «Доска» → «Доска связей», тест темы доски удалить (тема — в
`ui.test.tsx`), тест «Обновить из Jira» доски переносится сюда.
- [ ] **Step 2:** `npx vitest run src/shell src/App.test.tsx src/projects` — FAIL.
- [ ] **Step 3:** реализовать `AppBar`, `DataStatus`; `Layout` рисует `<AppBar>` и `<main className="page">`;
`NewProjectDialog` отдаёт `batch_id` из ответа, `Layout` вызывает `refresh.track(batchId)`; убрать кнопку и тему из
`Toolbar`.
- [ ] **Step 4:** `npx vitest run`, `npm run lint` — зелёные; `npm run e2e` — зелёный.
- [ ] **Step 5: Commit** `feat(web): верхняя панель — разделы, данные Jira, обновление, тема (#65)`
### Task 6: Шапка и панель управления доски, легенда
**Files:**
- Create: `web/src/board/components/BoardHeader.tsx` (вместо `Toolbar.tsx`, файл удалить), `web/src/board/legendPref.ts`
- Modify: `web/src/board/BoardPage.tsx`, `web/src/board/components/FilterBar.tsx`, `web/src/board/components/Legend.tsx`,
`web/src/board/components/MultiSelect.tsx`, `web/src/board/components/ProblemsDrawer.tsx`,
`web/src/board/components/ExportBox.tsx`, `web/src/board/components/LinkTable.tsx`, `web/src/board/board.css`,
`web/src/board/BoardPage.test.tsx`, `web/e2e/board.spec.ts`
**Interfaces:**
- Consumes: `PageHeader`, `Panel`, `Button`, `Badge`, `Menu`/`MenuItem`/`MenuSeparator`, `Notice`, иконки.
- Produces: `BoardHeader(p)` — пропсы нынешнего `Toolbar` без `refreshing`/`onRefresh`; `readLegendOpen(): boolean`,
`saveLegendOpen(open: boolean): void` (`localStorage["jira-hub-legend"]`, `"0"` — свёрнута, ошибки хранилища
глотаются); `Legend({prefs, open, onToggle})`.
- Разметка (design.md, решения 13–15): липкая шапка `.sticky` (сохранить `headRef` и `--head-h`) → `PageHeader
title="Доска связей"` с подзаголовком-счётчиками (тексты счётчиков — нынешние `.stats`), источник — `Badge`
«из файла: <имя>» + `IconButton label="Вернуться к данным хаба"` (= `onReload`), событие доски — `statusText` с тоном
(`role="status"`); `actions`: `Button secondary icon={<IconWarning/>} aria-pressed={problemsOpen}` «Проблемы» +
`Badge tone={n ? "error" : "neutral"}` со счётчиком, `Menu label="Ещё"`: «Перечитать», «Загрузить файл…» (скрытый
`<input type=file>` с прежним `accept`, пункт вызывает `click()`), «Экспорт JSON», «Таблица связей» (`pressed`),
«Привязка к сетке» (`pressed`), `MenuSeparator`, «Сбросить раскладку» (`danger`, прежнее подтверждение внутри меню).
Под шапкой — `Panel` `.board-controls`: `MultiSelect` спринтов и исполнителей (кнопка — вид `Select`, подпись
прежняя), разделитель, группа «Показать» (подпись 12/16 вторичным + кнопки `Button secondary aria-pressed`
«Соседи по связям» `IconNeighbors`, «Только передачи» `IconHandoff`, «Скрыть без связей» `IconLink`), группа «Режим»
(«Критический путь» `IconCritical`, «По датам» `IconCalendar`); `title` — нынешние. Легенда: `Button ghost
icon={<IconLegend/>} aria-expanded` «Легенда» + `Badge` «скрыто: N» (N = `hideKind.size + hideEdge.size`, только при
N > 0) и под ней пункты (прежняя логика). Ящик «Проблемы», экспорт, таблица связей — в `Panel`, заголовки 16/24
Semibold, ключи моно цветом действия.
- [ ] **Step 1: Тесты `BoardPage.test.tsx`:** обновить подписи (кнопки режимов — «Соседи по связям», «Только
передачи», «Скрыть без связей», «Критический путь», «По датам»; ключи `graph-*` в хранилище — прежние); меню «Ещё» →
«Сбросить раскладку» с подтверждением; «Загрузить файл…» загружает CSV — `Badge` «из файла: jira-export.csv», кнопка
«Вернуться к данным хаба» возвращает хаб; «Проблемы» — `aria-pressed` и счётчик; новый «Свёрнутая легенда» (спека):
скрыт «тестирование», «Легенда» свёрнута → после перемонтирования свёрнута, «скрыто: 1», `jira-hub-legend` = `"0"`;
на доске нет «Обновить из Jira» и пункта темы.
- [ ] **Step 2:** `npx vitest run src/board/BoardPage.test.tsx` — FAIL.
- [ ] **Step 3:** реализовать; `e2e/board.spec.ts` — `"▥ по датам"` → `"По датам"` и прочие подписи.
- [ ] **Step 4:** `npx vitest run`, `npm run lint`, `npm run e2e` — зелёные.
- [ ] **Step 5: Commit** `feat(web): шапка доски, панель управления по группам, сворачиваемая легенда (#65)`
### Task 7: Полотно доски в стиле системы
**Files:**
- Modify: `web/src/board/board.css` (разделы 6–9: полотно, дорожки, полоса дат, прокрутка, карточки, ящик),
`web/src/board/components/Card.tsx` (значок «Открыть карточку» → `IconOpen` 16, `aria-label` прежний),
`web/src/board/components/Lane.tsx` (кнопки дорожки — иконки, подписи прежние)
**Interfaces:**
- Consumes: токены доски Task 1, иконки Task 2.
- Вид (design.md, решение 16): `.wrap` — поверхность, граница, радиус 16; дорожки — чередование `--lane-alt`, подпись
дорожки 12/16 Semibold вторичным (без `text-transform`); карточка — радиус 12, отступ 12, граница
`--color-border-default`, hover — `--color-border-strong`; ключ — моно 13 px Semibold цветом действия; статус —
вид `Badge`; `.card.crit` — кольцо `--guide`; ручки и уголок — цвет действия; полоса дат — поверхность, подписи
12/16 Semibold; `.hscroll` — бегунок `--color-border-strong`, hover — действие. Размеры карточек (`CW`, высоты,
`.measure`) и классы, на которые опираются тесты (`card`, `grip`, `rsz`, `dcol`, `dlabel`, `closed`, `out`) — прежние.
- [ ] **Step 1:** `npx vitest run src/board` — зелёный до правок (точка отсчёта).
- [ ] **Step 2:** переписать стили и иконки.
- [ ] **Step 3:** `npx vitest run`, `npm run lint`, `npm run e2e` (перетаскивание, растяжка, «по датам», приглушение
закрытых) — зелёные; `src/styles/tokens.test.ts` — зелёный.
- [ ] **Step 4: Commit** `style(web): полотно доски — карточки, дорожки, даты в стиле системы (#65)`
### Task 8: Список задач
**Files:**
- Create: `web/src/issues/IssueFilters.tsx`, `web/src/issues/IssueTable.tsx`
- Modify: `web/src/issues/IssuesPage.tsx`, `web/src/issues/issues.css`, `web/src/issues/IssuesPage.test.tsx`,
`web/e2e/issues.spec.ts`
**Interfaces:**
- Consumes: `PageHeader`, `Panel`, `SearchInput`, `Segmented`, `Button`, `Badge`, `Chip`, `Field`, `Select`,
`Notice`, `IssueLink`, `isClosed`, `dueOf`, `withCurrent`, `filterOptions`, `epicNames`, `isOverdue`/`todayISO`
(`issues/card.ts`).
- Produces: `IssueFilters({filters, set, q, setQ, showClosed, opts, hasBoard})` — вся панель фильтров;
`IssueTable({rows, epicNames, closed, onOpen})`, где `onOpen(key)` — тот же переход, что `IssueLink`
(вынести из `IssueLink.tsx` хук `useOpenIssue(): (key: string) => void`, `IssueLink` пользуется им же).
- Разметка (design.md, решения 17–18): `PageHeader title="Задачи" subtitle="показано N"` (+ « · есть ещё» при
`hasNextPage`); `Panel` фильтров: `SearchInput label="Поиск по ключу или теме" placeholder="Ключ или тема"`,
`Segmented label="Закрытые задачи"` «Открытые» / «Все» (`closed=1`), `Button secondary aria-pressed` «Только
соседи» (`neighbor=1`), `Button secondary icon={<IconFilter/>} aria-expanded` «Фильтры» + `Badge` с числом выбранных
из шести; строка плашек: `Chip` «<подпись>: <значение>» (`removeLabel="Снять фильтр <подпись>"`) для каждого
выбранного, `Button ghost` «Сбросить»; раскрытая панель — сетка 3 колонки (2 уже 900 px, 1 уже 640 px) из `Field` +
`Select` (`aria-label` прежний — подпись, варианты и «(нет на доске)» прежние). Раскрыта ли —
`sessionStorage["jira-hub-issue-filters"]` (`"1"`), по умолчанию свёрнута. Таблица — `Panel padding="none"`, шапка
липкая, колонки в порядке спеки; ключ + `Badge` «сосед»; статус — `Badge`; срок просроченной незакрытой задачи —
класс `overdue` (цвет ошибки) по `isOverdue(due, status, closed, todayISO())`; строка — `onClick`: если
`window.getSelection()?.toString()` пусто и цель не внутри `a, button` → `onOpen(key)`; `cursor:pointer`, hover —
`--color-bg-hover`. «Показать ещё» — `Button secondary` по центру; пусто — `Notice tone="info"` «По этим фильтрам
задач нет»; ошибка — `Notice tone="error"` с `action` «Повторить».
- [ ] **Step 1: Тесты `IssuesPage.test.tsx`:** прежние сценарии на новых ролях («Открытые»/«Все» — `radio`, адрес
`closed=1`, запрос с/без `hide_closed=1`; выбранный закрытый статус); «Фильтр в адресе» (спека): адрес со
`sprint=` и `assignee=` → кнопка «Фильтры» со счётчиком 2, плашки «Спринт: AWPCA_Спринт 2» и «Исполнитель: …»,
панель свёрнута; «Снять фильтр плашкой» (спека); «Сбросить» снимает шесть списков, `q` и `neighbor` остаются;
«Щелчок по строке» (спека) — щелчок по ячейке темы → адрес `/issues/AURORA-…`; выделенный текст в строке — без
перехода; «просрочено» — класс `overdue` у срока незакрытой задачи с прошедшим сроком, у закрытой нет.
- [ ] **Step 2:** `npx vitest run src/issues/IssuesPage.test.tsx` — FAIL.
- [ ] **Step 3:** реализовать; `e2e/issues.spec.ts` — новые подписи.
- [ ] **Step 4:** `npx vitest run`, `npm run lint`, `npm run e2e` — зелёные.
- [ ] **Step 5: Commit** `feat(web): список задач — поиск на виду, фильтры панелью с плашками, строка открывает карточку (#65)`
### Task 9: Карточка задачи и заметки
**Files:**
- Modify: `web/src/issues/IssueModal.tsx` (на `Dialog size="lg"`, поведение закрытия — прежнее),
`web/src/issues/IssueCard.tsx`, `web/src/issues/Notes.tsx`, `web/src/issues/NoteForm.tsx`,
`web/src/issues/issues.css`, `web/src/issues/IssueCard.test.tsx`, `web/src/issues/IssueModal.test.tsx`
**Interfaces:**
- Consumes: `Dialog`, `Button`, `IconButton`, `Badge`, `Panel`, `Field`, `Select`, `Textarea`, `Notice`, иконки.
- Вид (design.md, решение 19): шапка — ключ моно 13 px вторичным, `Badge` статуса, справа `Button secondary
icon={<IconExternal/>}` «Открыть в Jira» (ссылка `<a>` с видом кнопки — класс `ui-btn ui-btn--secondary`) и
`IconButton label="Закрыть"`; тема `<h2>` 20/28 Bold; факты 14/20 вторичным, «просрочено» — `Badge tone="error"`;
заголовки разделов `<h3>` 16/24 Semibold без верхнего регистра; кнопки «исходник»/«текст», «развернуть», «Скрыть»/
«Показать» — `Button ghost` 32 px; правая колонка — `Panel` с фоном `--color-bg-subtle`; форма заметок — `Field` +
`Select`/`Textarea`, «Сохранить» `primary`, «Отмена» `secondary`, «Править» `secondary`, ошибка — `Notice error`.
Содержание, порядок, роли и тексты, на которые опираются тесты (`data-testid`, «исходник», «Скрыть», «Править»,
«Сохранить»), — прежние; «Открыть в Jira ↗» → «Открыть в Jira» (иконка вместо стрелки).
- [ ] **Step 1:** обновить в тестах только изменившиеся подписи («Открыть в Jira»); `npx vitest run src/issues` — FAIL
там, где подпись изменилась.
- [ ] **Step 2:** переписать разметку и стили.
- [ ] **Step 3:** `npx vitest run`, `npm run lint`, `npm run e2e` — зелёные.
- [ ] **Step 4: Commit** `style(web): карточка задачи и служебные заметки в стиле системы (#65)`
### Task 10: Вход, «Новый проект», служебные экраны, уборка старых стилей
**Files:**
- Modify: `web/src/auth/LoginScreen.tsx`, `web/src/projects/NewProjectDialog.tsx`, `web/src/projects/ProjectGate.tsx`,
`web/src/projects/projects.css`, `web/src/shell/NotFound.tsx`, `web/src/shell/ErrorBoundary.tsx`,
`web/src/styles/base.css`, `web/src/styles/controls.css` (удалить, если всё перенесено в `ui/ui.css`),
`web/src/styles/tokens.css` (удалить псевдонимы `--bg`, `--fg`, `--muted`, `--line`, `--card`, `--panel`, `--soft`,
`--accent`, `--accent-fg`), все `*.css`, где они остались, `web/src/auth/LoginScreen.test.tsx`,
`web/src/projects/projects.test.tsx`, `web/src/App.test.tsx`, `ARCHITECTURE.md`
**Interfaces:**
- Вид (design.md, решение 20): вход — `Panel` 480 px по центру, `<h2>` «Вход в хаб» 20/28, пояснение, `Field
label="Клиентский токен хаба"` + `TextInput size="lg"`, `Button primary size="lg"` «Войти», ошибка — `Notice
error`; «Новый проект» — `Dialog size="sm" label="Новый проект"`, поля через `Field`, «Создать» `primary`, «Отмена»
`secondary`, ошибка хаба — `Notice error` у кнопок; `ProjectGate` — `Notice` («проект X не найден» — warning с
подсказкой; «Проектов нет» — info с `Button primary` «+ Новый проект…»; ошибка — error); «не найдено» и ошибка
отрисовки — `Notice` в контейнере, ссылки «Доска связей», «Задачи» / `Button` «Перезагрузить». Классы `.btn`,
`.link`, `.dd-*` из `controls.css` больше нигде не используются (поиск `className="btn` и `"link"` пуст).
- `ARCHITECTURE.md`, абзац `web/`: токены `web/src/styles/tokens.css`, компоненты `web/src/ui/`, обновление из Jira —
`web/src/shell/refresh.ts` (провайдер в `App.tsx`), доска — только источник и события снимка.
- [ ] **Step 1:** обновить подписи в тестах (роли и тексты входа, «Новый проект», «не найдено» — прежние, кроме
ссылок разделов); `npx vitest run` — FAIL там, где изменилось.
- [ ] **Step 2:** переписать экраны; удалить псевдонимы и `controls.css`; заменить оставшиеся `var(--bg)` и прочие
на семантические имена.
- [ ] **Step 3:** `grep -rn "var(--\(bg\|fg\|muted\|line\|card\|panel\|soft\|accent\|accent-fg\))" web/src` — пусто;
`npx vitest run`, `npm run lint`, `npm run e2e`, `make check` — зелёные.
- [ ] **Step 4: Commit** `style(web): вход, новый проект, служебные экраны; старые стили убраны (#65)`
### Task 11: Скриншоты и итоговая проверка
**Files:**
- Create: `web/e2e/screens.spec.ts`
- Modify: `.gitignore` (каталог `web/e2e-screens/`), `web/playwright.config.ts` (если нужен проект для `SCREENS=1`)
**Interfaces:**
- `screens.spec.ts`: весь файл — `test.skip(!process.env.SCREENS)`; для тем `light` и `dark` (через
`localStorage["graph-theme"]` до загрузки) и вьюпортов 1920×1080 и 390×844 снимает `fullPage` в
`web/e2e-screens/<тема>-<ширина>-<экран>.png`: `login`, `board`, `board-bydate` (режим «По датам»),
`board-problems` (ящик открыт), `issues`, `issues-filters` (панель раскрыта, один фильтр выбран), `card`
(`/app/issues/AURORA-9001` поверх списка), `new-project` (окно открыто), `refresh` (пакет в очереди — без агента).
- [ ] **Step 1:** написать спецификацию; `cd web && SCREENS=1 npx playwright test e2e/screens.spec.ts` — 36 файлов
в `web/e2e-screens/`.
- [ ] **Step 2:** просмотреть снимки (Read по файлам): нет наложений, обрезанных рамок и текста у границ, тёмная тема
без светлых пятен (окно, меню, `<select>`), узкий экран без горизонтальной прокрутки страницы (кроме полотна
доски); найденное — исправить в стилях соответствующей задачи и переснять.
- [ ] **Step 3:** `make check` и `cd web && npm run e2e` — зелёные; `git status` — снимков в индексе нет.
- [ ] **Step 4: Commit** `test(web): скрипт скриншотов экранов для проверки вида (#65)`

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-21

View File

@ -0,0 +1,189 @@
## Context
`main` 57bf1fa. Материалы дизайн-системы (вне репозитория, `~/projects/jira-bridge/design/`):
- `DESIGN-SYSTEM-RULES.md`, `STYLE-INVENTORY.md`, `REACT-COMPONENT-CATALOG.md` — правила и инвентарь AURA;
- `AURA-xlsx-tool-handoff-2026-09-29.zip` — исходники AURA: `design-system/tokens.json` (канон токенов),
`src/styles/tokens.css` (сгенерированные CSS-переменные), `src/styles.css`, `design-system/foundations.md`;
- `Overview.svg`, `Универсалки-2…7.svg` — экспорт Figma «Универсалки»: основы (цветовые роли, типографика, геометрия),
загрузка файлов, составные компоненты (плитки выбора, сообщения, панель действия), макеты, контейнер контента,
базовые компоненты (кнопки, табы, шаги, тема, навигация приложений).
Что важно из системы:
- **Цвета (светлая):** канва `#f8f8f6`, поверхность `#fdfdfc`, приглушённый фон `#fafaf8`, текст `#17171a`, вторичный
`#818190`, плейсхолдер `#5c6260`, граница `#e5e5e0`, сильная граница `#cfcfc8`, действие `#568d32` (hover/active
`#477729`), выбранное — фон `#f4faf5` и граница `#568d32`, ошибка `#b42318` / фон `#fff1f0`, предупреждение `#d89614`
/ фон `#fffbe6` / граница `#ffe58f`, фокус `rgb(42 122 70 / 35%)`. **Тёмная:** канва `#181818`, поверхность `#1f1f1f`,
приглушённый `#272727`, текст `#f2f4f2`, вторичный `#d6dcd7`, плейсхолдер `#aab2ac`, границы `rgb(232 238 233 / 10%)` и
`/ 17%`, действие `#69a93d` (hover `#83c657`), текст на действии `#112016`, ошибка `#e26c67` / фон `#402725`,
предупреждение `#e8c17a` / фон `#382f20` / граница `#8a6d1d`, фокус `rgb(116 184 255 / 55%)`.
- **Типографика:** заголовок раздела Bold 20/28, текст Regular 14/20, подпись Semibold 14/20, мелкий текст 12/16,
крупный 16/24.
- **Геометрия:** шаг 4 px; контрол 32 px (крупный 40), радиус контрола 10; кнопки — «таблетки» (радиус 999);
панели — радиус 16, отступ 16–24; окно/канва — радиус 20–24; флажок 16×16 радиус 4; контейнер контента 1440 px;
переходы 180 ms ease, нажатие 100 ms.
- **Компоненты из Figma:** кнопки (основная зелёная, вторичная контурная, опасная, с загрузкой), сегментный
переключатель на сером треке с белой активной «таблеткой» и зелёным текстом («Навигация приложений»), переключатель
темы (солнце / луна), сообщения успеха и ошибки (иконка в круге, заголовок, пояснение), плитки выбора, плашки
файлов, шаги процесса.
Приложение сейчас (`web/src`): глобальные стили `styles/{theme,base,controls}.css` (переменные `--bg`, `--fg`,
`--muted`, `--line`, `--card`, `--panel`, `--soft`, `--accent` синий `#1f6f8b`, палитра видов работ `--k-*`, приоритетов
`--p-*`, стрелок `--edge*`, `--guide`), стили страниц `board/board.css` (165 строк, в нём же вход, список, карточка),
`issues/issues.css`, `projects/projects.css`, `shell/shell.css`. Шрифт IBM Plex Sans/Mono из `@fontsource`. Иконки —
символы текста (⋯ ⇄ ⚑ ▥ ✕ ▼). Обновление из Jira, ход пакета и тема живут в `board/useBoardSource.ts` и
`board/components/Toolbar.tsx`; тема — `shell/theme.ts` (ключ `graph-theme`: `""` системная, `light`, `dark`).
## Goals / Non-Goals
**Goals:** приложение выглядит как AURA в обеих темах; каждый экран собран из одних компонентов и токенов; общие
действия (обновление из Jira, тема, токен) — в оболочке; фильтры доски и списка сгруппированы и не перегружают экран;
поведение, адреса, ключи `localStorage` и логика доски — прежние.
**Non-Goals:** см. proposal.md.
## Decisions
1. **Без Ant Design.** Приложение на своих компонентах; тянуть antd ради внешнего вида — тяжёлая зависимость и
переписывание всех контролов. Токены AURA переносятся в `web/src/styles/tokens.css` вручную с пометкой источника
(`tokens.json` 29.09.2026); скрипта синхронизации нет.
2. **Токены — два слоя.** Семантические имена AURA (`--color-bg-canvas`, `--color-text-secondary`,
`--color-action-primary`, `--space-*`, `--radius-*`, `--font-size-*`, `--component-control-height` …) для светлой и
тёмной темы плюс токены приложения, которых в AURA нет: палитра видов работ `--k-*`, приоритеты `--p-*`, стрелки
`--edge`, `--edge-strong`, `--guide`, дорожки доски. Старые имена (`--bg`, `--fg`, `--muted`, `--line`, `--card`,
`--panel`, `--soft`, `--accent`, `--accent-fg`) на время перехода — псевдонимы семантических; после переноса всех
страниц псевдонимы удаляются. `--edge-strong` и `--accent` становятся зелёными; `--guide` (передачи, критический
путь) остаётся оранжевым — его роль — выделение поверх зелёного. Палитра `--k-*` подбирается заново под тёплую
канву, оттенки остаются различимыми между собой и с зелёным действием.
3. **Цвета только из токенов.** Вне `tokens.css` в CSS нет литералов цвета (`#…`, `rgb(…)`, `hsl(…)`, именованных,
кроме `transparent`, `currentColor`, `inherit`); проверка — тест Vitest, который читает все `*.css` в `web/src`.
Так новая страница не может «забыть» тему.
4. **Шрифт — Mulish** (`@fontsource/mulish` 5.3.0, начертания 400/600/700, подмножества с кириллицей): ближайшая
свободная гарнитура к Proxima Nova по пропорциям; Montserrat шире и съедает место в плотных таблицах. Моноширинный —
IBM Plex Mono (ключи задач, код, исходник разметки), как сейчас. IBM Plex Sans удаляется из зависимостей.
5. **Иконки — свои SVG** в `web/src/ui/icons.tsx` (линейные 16/20 px, обводка 1.5, `currentColor`), набор под
нужды приложения: обновить, загрузка/файл, меню (⋯), закрыть, шеврон, поиск, фильтр, солнце, луна, внешняя ссылка,
предупреждение, галочка, ошибка, передачи, критический путь, по датам, соседи, связи, сетка, сброс, легенда. Без
новой зависимости; текстовые символы в интерфейсе заменяются иконками.
6. **Компоненты `web/src/ui/`:** `Button` (варианты `primary`, `secondary`, `danger`, `ghost`; размеры 32 и 40;
«таблетка»; состояние загрузки — крутилка вместо иконки, кнопка недоступна), `IconButton` (32×32, радиус 10, имя —
`aria-label`), `Segmented` (трек `--color-bg-subtle`/серый, активный сегмент — поверхность и зелёный текст; ссылки
или кнопки), `ThemeSwitch` (солнце/луна), `Field` (подпись Semibold 14/20 над контролом), `TextInput`, `SearchInput`
(иконка поиска, очистка), `Select` (родной `<select>`, 32 px, радиус 10, своя стрелка), `Checkbox` (16×16, радиус 4,
зелёный при отметке), `Badge` (статус, сосед, счётчик), `Chip` (снимаемая плашка фильтра: «Спринт: X ✕»), `Notice`
(успех / ошибка / предупреждение / сведения: иконка, текст, необязательное действие), `Panel` (поверхность, граница,
радиус 16), `PageHeader` (заголовок 20/28, подзаголовок 14/20 вторичным, место для действий справа), `Dialog`
(родной `<dialog>`, радиус 20, тень канвы), `Menu` (выпадающее меню на основе нынешнего `Dropdown`), `Popover`.
Компоненты держат только вид и доступность; логика страниц остаётся в страницах.
7. **Состояние «предупреждение» разрешено.** В AURA его нет в контролах, но токены `warning` в системе есть, а хабу
нужно отличать «агент офлайн», «хаб не отвечает — прежние данные» от ошибок. Используется только в `Notice`, `Badge`
и тексте состояния; у полей ввода состояния `warning` нет.
8. **Контейнер.** Страницы «Задачи», «не найдено», вход — контейнер до 1440 px по центру с боковым отступом 24 px
(16 px уже 640 px). Доска — во всю ширину окна с отступом 24 px: графу нужна ширина. Верхняя панель — во всю ширину,
её содержимое выровнено по тем же отступам.
9. **Верхняя панель (оболочка)**, высота 64 px, поверхность, нижняя граница; слева направо: название «Хаб Jira»
(Bold 16/24), выбор проекта (`Select`), разделы — `Segmented` из ссылок «Доска связей» / «Задачи» (активный —
`aria-current="page"`), растяжка, блок данных Jira, `ThemeSwitch`, меню `IconButton ⋯` с пунктом «Сменить токен».
Уже 1100 px блок данных переносится второй строкой панели; уже 640 px — название скрыто, разделы на всю ширину.
10. **Блок данных Jira:** точка и подпись агента («агент на связи» зелёной точкой / «агент офлайн» — предупреждение;
`GET /agent` при старте и раз в 60 с, подсказка — время последнего выхода на связь), текст «данные на ДД.ММ ЧЧ:ММ»
(`last_sync.finished_at` из снимка доски текущего проекта; «полного обновления не было»), кнопка `primary`
«Обновить из Jira» с иконкой; во время пакета — состояние загрузки и текст хода «пакет #N в очереди / выполняется,
~N с»; итог (выполнен / не выполнен: текст / отменён / потерян) — текстом с цветом статуса до следующего события
или 10 с для «выполнен». Тексты событий — нынешние `statusText` для пакетных событий.
11. **Обновление из Jira переезжает в оболочку** (`web/src/shell/refresh.ts` — хук и контекст): постановка
`POST /refresh {full: true, author: "viewer"}` с проверкой `GET /agent`, один опрос `GET /batches/{id}` раз в 3 с,
остановка на терминальном статусе, 4xx, 404 и после 5 ошибок «хаб не отвечает» подряд — правила прежние (M-2…M-4
#20). Подхват незавершённого пакета `viewer` с первой командой `refresh` — при входе в приложение и после каждой
успешной загрузки снимка доски (подписка на кэш запросов), кроме перечитывания после своего пакета. По завершении
пакета инвалидируются снимки всех досок (`["board"]`), списки задач (`["issues"]`), карточки (`["issue"]`) и
проекты (`["projects"]`, счётчики задач). Доска, показывающая файл, файл не заменяет (M-1). «Новый проект» отдаёт
`batch_id` созданного пакета прямо в оболочку — опрос начинается сразу, без подхвата.
12. **Доска после переезда** (`useBoardSource`): источник (хаб / файл), `reload`, `loadFile`, события снимка
(загрузка, хаб не отвечает, файл не разобран, JSON применён, ошибка) — как были; пакетных событий нет. Строка
состояния доски показывает только свои события, пакетные — в оболочке.
13. **Шапка доски** (липкая, как сейчас): `PageHeader` «Доска связей», подзаголовок — счётчики («42 задачи из 120 ·
+5 соседей · 60 связей из 80 · скрыто без связей: 3»), источник — `Badge` («хаб» не показывается; «из файла:
имя» с кнопкой ✕ = «Перечитать»), событие доски — текстом под заголовком; справа — `Button secondary` «Проблемы»
со счётчиком (`Badge` ошибки, если > 0) и меню ⋯: «Перечитать», «Загрузить файл…», «Экспорт JSON», «Таблица
связей» (переключатель), «Привязка к сетке» (переключатель), разделитель, «Сбросить раскладку» (с прежним
подтверждением). Темы в меню доски нет.
14. **Панель управления доски** — `Panel` в одну строку (перенос по ширине): «Спринты» и «Исполнители» —
мультиселекты (нынешний `MultiSelect`, кнопка в виде `Select` с подписью внутри), разделитель, группа «Показать»:
переключатели «Соседи по связям», «Только передачи», «Скрыть без связей»; группа «Режим»: «Критический путь», «По
датам». Переключатель — кнопка-«таблетка» с иконкой, включённый — стиль «выбрано» (фон `--color-bg-selected-green`,
зелёная граница и текст), `aria-pressed`. Подсказки (`title`) — нынешние.
15. **Легенда** — под панелью, сворачивается кнопкой «Легенда» (`aria-expanded`); свёрнута — видно число скрытых
видов работ и типов связей («скрыто: 2»), если они есть. Состояние свёртки — `localStorage` ключ
`jira-hub-legend` (`"0"` свёрнута; по умолчанию развёрнута). Поведение пунктов (щелчок скрывает вид / тип) —
прежнее.
16. **Карточки на доске:** радиус 12, граница `--color-border-default`, поверхность; ключ — моно, цвет действия;
статус — `Badge`; размеры, ручки перетаскивания, растяжка и измерение высоты — прежние (меняются только цвета,
радиусы и шрифт; высота по содержимому считается тем же слоем измерения, поэтому смена шрифта не ломает раскладку,
а сохранённые размеры карточек остаются). Дорожки — чередование через `--color-bg-subtle`.
17. **Список задач.** `PageHeader` «Задачи», подзаголовок «показано N» (и «есть ещё», если следующая порция есть).
Под ним `Panel` фильтров: строка 1 — `SearchInput` «Ключ или тема» (широкий), `Segmented` «Открытые | Все»
(замена флажка «скрыть закрытые», адрес тот же: «Все» — `closed=1`), переключатель-«таблетка» «Только соседи»
(`neighbor=1`), кнопка «Фильтры» со счётчиком выбранных из шести списков; строка 2 (при выбранных) — `Chip`
«Спринт: X ✕» на каждый выбранный список и «Сбросить» (снимает шесть списков, поиск и переключатели не трогает);
панель «Фильтры» раскрывается под строкой сеткой `Field`+`Select` по 3 в ряд (спринт, исполнитель, вид работы,
статус Jira, метка, эпик). Раскрыта ли панель — в `sessionStorage` (`jira-hub-issue-filters`). Подписи и варианты
списков, «(нет на доске)» — как сейчас.
18. **Таблица задач** — в `Panel` без внутреннего отступа, шапка липкая; колонки: Ключ (моно, ссылка; рядом `Badge`
«сосед»), Тема, Статус Jira (`Badge`), Исполнитель, Срок (просрочено — цветом ошибки, по правилу карточки), Спринт,
Эпик, Вид работы, Тип. Строка 44 px, при наведении — приглушённый фон; щелчок по строке (кроме ссылок и выделения
текста) открывает карточку тем же переходом, что ключ. «Показать ещё» — `Button secondary` по центру под таблицей.
Пусто — `Notice` сведений «По этим фильтрам задач нет».
19. **Карточка задачи** — `Dialog` 1000 px, радиус 20; шапка: ключ (моно), `Badge` статуса, справа `Button secondary`
«Открыть в Jira» с иконкой внешней ссылки и `IconButton` «Закрыть»; тема — 20/28 Bold; строка фактов — 14/20
вторичным, «просрочено» — `Badge` ошибки. Разделы — заголовки 16/24 Semibold (без верхнего регистра); правая
колонка — `Panel` приглушённого фона: заметки, прочие поля, команды. Форма заметок — `Field`/`Select`/`textarea`
системы, «Сохранить» `primary`, «Отмена» `secondary`. Содержание, порядок и поведение разделов — прежние.
20. **Вход** — `Panel` 480 px по центру канвы: заголовок 20/28, пояснение, поле 40 px, `Button primary` «Войти»
(40 px); ошибка — `Notice` ошибки. **«Новый проект»** — `Dialog` 480 px с `Field`-полями, «Создать» `primary`,
«Отмена» `secondary`, ошибка хаба — `Notice` у кнопок. **«Не найдено»**, **ошибка отрисовки**, **проект не
найден**, **нет проектов** — `Notice` в контейнере с действием.
21. **Тема** — `ThemeSwitch` в оболочке, два положения. Ключ `graph-theme` прежний: без сохранённого выбора (или `""`)
— тема системы, переключатель показывает её; щелчок сохраняет `light` или `dark`. Пункта «Тема: системная» больше
нет: вернуть «как в системе» — убрать выбор нельзя, это осознанное упрощение (вторичный сценарий).
22. **Фокус и доступность:** видимое кольцо фокуса 2 px `--color-focus-ring` со смещением 2 px у всех интерактивных
элементов; цвет не единственный признак состояния (включённые переключатели — `aria-pressed` и иконка-галочка или
заливка; ошибки — иконка); `prefers-reduced-motion` отключает переходы.
23. **Проверка вида.** Скриншоты Playwright на тестовом хабе (`web/scripts/test-hub.sh`) 1920×1080 и 390×844 в
светлой и тёмной темах: доска, задачи с раскрытыми фильтрами, карточка, вход, «Новый проект» — прикладываются к PR
(в репозиторий не попадают, снимаются скриптом `web/e2e/screens.spec.ts` по флагу `SCREENS=1`). Пиксельных
эталонов в наборе тестов нет: вид будет меняться по замечаниям владельца.
24. **Тесты.** Логика доски и golden-эталоны не меняются. Тесты, привязанные к разметке (`Toolbar`, `BoardPage`,
`IssuesPage`, `LoginScreen`, `projects`, e2e), переписываются под новые роли и подписи; тесты опроса пакета
переезжают из `useBoardSource.test.tsx` в тест хука оболочки. Новые: компоненты `ui/` (роли, `aria-*`, состояния),
тест «цвета только из токенов», тест переключателя темы.
## Risks / Trade-offs
- **Шрифт меняет ширину текста** → сохранённые высоты карточек на доске могут обрезать текст. Смягчение: высота «по
содержимому» пересчитывается двойным щелчком по уголку, как сейчас; «Сбросить раскладку» доступна.
- **Переезд обновления в оболочку** затрагивает отработанные правила опроса (M-1…M-5 #20). Смягчение: перенос кода и
тестов почти без изменений, меняется только владелец состояния и набор инвалидируемых запросов.
- **Большой дифф стилей** → регрессии вида на редких состояниях (ящик «Проблемы», режим «по датам», экспорт).
Смягчение: скриншоты всех экранов в PR, владелец смотрит живое приложение.
## Decisions made without discussion
Владелец 21.10.2026 отдал редизайн и UX на усмотрение агента («сделай редизайн и проработай UX сам, потом будем
отталкиваться»); его входные данные — только: «и то и то» (родство с AURA и исправление неудобств), примеры
неудобств (обновление из Jira только с доски, перегрузка фильтрами, «падинги не те»), сценарий (раз в спринт граф,
затем поиск и перепроверка задач), «фильтры все нужны». Всё ниже — решения агента.
| # | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 21.10.2026 | Без Ant Design, токены вручную (решение 1) | antd — замена всех контролов и тяжёлая зависимость ради вида | низкая: вид тот же, antd можно добавить позже |
| 2 | 21.10.2026 | Mulish вместо Proxima Nova (решение 4) | Proxima Nova платная; Mulish ближе по пропорциям, с кириллицей | низкая: замена шрифта — одна строка токена и пакет |
| 3 | 21.10.2026 | Свои SVG-иконки без библиотеки (решение 5) | ~20 иконок, зависимость не нужна | низкая |
| 4 | 21.10.2026 | Предупреждение разрешено вне контролов (решение 7) | хабу нужно отличать «офлайн» и «прежние данные» от ошибок | низкая |
| 5 | 21.10.2026 | Доска во всю ширину, прочее — контейнер 1440 (решение 8) | графу нужна ширина, таблице и карточке — ограничение строки | низкая |
| 6 | 21.10.2026 | Обновление из Jira и ход пакета — в оболочке, инвалидация всех данных (решения 10–12) | пример неудобства владельца; после обновления устаревают и список, и карточка | средняя: правила опроса переносятся, регрессии ловят перенесённые тесты |
| 7 | 21.10.2026 | Редкие действия доски — в меню ⋯, режимы — группами «Показать» / «Режим» (решения 13–14) | «перегрузка фильтрами»; доска нужна раз в спринт, режимы — основное, файл/экспорт — редко | низкая: перестановка кнопок |
| 8 | 21.10.2026 | Легенда сворачивается, состояние в `localStorage` (решение 15) | легенда занимает строку, нужна для чтения цветов — свёрнута по выбору | низкая |
| 9 | 21.10.2026 | Список: поиск и «Открытые / Все» на виду, шесть списков — в панели «Фильтры» с плашками (решение 17) | частые действия на виду, редкие — по запросу; выбранное видно плашками | низкая |
| 10 | 21.10.2026 | Щелчок по строке таблицы открывает карточку (решение 18) | основной сценарий списка — открыть и перепроверить задачу | низкая |
| 11 | 21.10.2026 | Тема — два положения, системная только без сохранённого выбора (решение 21) | переключатель AURA двухпозиционный | низкая: вернуть третье положение — пункт меню |
| 12 | 21.10.2026 | Проверка вида скриншотами в PR, без пиксельных эталонов (решение 23) | вид будет меняться по замечаниям, эталоны мешали бы | низкая |

View File

@ -0,0 +1,40 @@
## Why
Веб-приложение хаба выросло из перенесённой доски `links.html` и выглядит и ведёт себя как набор страниц, собранных в
разное время: свои цвета и отступы у каждой страницы, «Обновить из Jira» и тема — только в шапке доски, фильтры списка
задач — сплошная строка из шести выпадающих списков, двух флажков и поиска. Владелец (21.10.2026, jira-hub#65) хочет,
чтобы хаб выглядел как родственное приложение AURA (дизайн-система из handoff AURA-xlsx-tool 29.09.2026 и Figma
«Универсалки», материалы — `~/projects/jira-bridge/design/`), и чтобы интерфейс стал логичнее.
Сценарий владельца: раз в спринт — обзор графа связей на доске; в остальное время — поиск и перепроверка задач в
списке и карточке. Все фильтры нужны, но упакованы должны быть удобнее. Редизайн и UX владелец отдал на усмотрение
агента; дальше — доработки от результата.
## What Changes
- **Дизайн-система в приложении:** токены AURA (цвета светлой и тёмной темы, типографика, шаг 4 px, радиусы, высоты
контролов, фокус) — `web/src/styles/tokens.css`; набор базовых компонентов `web/src/ui/` (кнопки-«таблетки»,
иконки, сегментный переключатель, переключатель темы, поле, выпадающий список, флажок, бейдж, сообщение, панель,
окно, меню); все страницы собраны из них. Шрифт — Mulish (свободная замена Proxima Nova, с кириллицей), моноширинный
— IBM Plex Mono, как был.
- **Верхняя панель приложения:** название, проект, разделы сегментным переключателем, блок данных Jira (время
последнего полного обновления, агент на связи / офлайн, «Обновить из Jira» и ход пакета), переключатель темы, меню
с «Сменить токен». Обновление из Jira и тема доступны с любой страницы.
- **Обновление из Jira — уровень приложения:** пакет `refresh` ставит и опрашивает оболочка; по завершении
перечитываются снимки доски, список задач и открытая карточка. Доска сохраняет только своё: источник (хаб / файл),
«Перечитать», загрузку файла и события снимка.
- **Доска:** шапка страницы (название, счётчики, источник, «Проблемы», меню), панель управления (спринты, исполнители,
режимы показа группами), сворачиваемая легенда; редкие действия — в меню. Поведение доски (модель, выборка,
критический путь, раскладка, ключи `localStorage`) не меняется.
- **Список задач:** поиск и «Открытые / Все» на виду, остальные фильтры — в раскрывающейся панели «Фильтры (N)»,
выбранные — снимаемыми плашками; таблица в панели, строка открывает карточку, «сосед» — плашкой у ключа. Адрес и
запросы к хабу — как были.
- **Карточка задачи, вход, «Новый проект», «не найдено», ошибки** — в стиле системы, содержание и поведение прежние.
## Non-Goals
- Ant Design и генерация токенов из `tokens.json` скриптом (`design-system:sync`): токены переносятся вручную.
- Proxima Nova (платный шрифт) и любые ресурсы вне сборки.
- Новые функции: правки в Jira из интерфейса (`viewer-edit`), страницы очереди, агента и настроек.
- Изменения API хаба, MCP, агента; изменения логики доски (golden-тесты не трогаются).
- Мобильная вёрстка доски сверх нынешней (узкий экран — только без поломок).

View File

@ -0,0 +1,320 @@
## ADDED Requirements
### Requirement: Дизайн-система
Приложение SHALL следовать дизайн-системе AURA (handoff AURA-xlsx-tool 29.09.2026, Figma «Универсалки») в светлой и
тёмной темах. Токены — цвета по семантическим ролям для обеих тем, типографика, отступы с шагом 4 px, радиусы, высоты
контролов, фокус, движение — SHALL быть определены в `web/src/styles/tokens.css` (вместе с токенами приложения: палитра
видов работ, приоритеты, стрелки и дорожки доски); прочие файлы стилей MUST NOT содержать литералов цвета и SHALL
брать цвета из токенов. Экраны SHALL собираться из компонентов `web/src/ui/` (кнопки, кнопка-иконка, сегментный
переключатель, переключатель темы, поле с подписью, поле ввода и поиска, выпадающий список, флажок, бейдж, снимаемая
плашка, сообщение, панель, шапка страницы, окно, меню). Геометрия: контролы 32 px (крупные 40 px) с радиусом 10 px,
кнопки — «таблетки», панели — радиус 16 px, окна — радиус 20 px, флажок 16×16 px; текст Regular 14/20, подписи
Semibold 14/20, заголовок страницы Bold 20/28. Страницы, кроме доски, SHALL занимать контейнер до 1440 px по центру с
боковым отступом 24 px (16 px на экранах уже 640 px); доска — всю ширину окна с отступом 24 px. Иконки — SVG из
сборки, не символы текста. Каждый интерактивный элемент SHALL иметь видимое кольцо фокуса; цвет MUST NOT быть
единственным признаком состояния.
#### Scenario: Цвет вне токенов
- **WHEN** в любом `*.css` в `web/src`, кроме `styles/tokens.css`, появился литерал цвета
- **THEN** тесты фронта падают с именем файла и литералом
#### Scenario: Тёмная тема
- **WHEN** выбрана тёмная тема
- **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`), как у прежней доски; без сохранённого выбора или со
значением `""` тема SHALL следовать настройке системы, и переключатель показывает её. Тема применяется до первой
отрисовки, без мигания.
#### Scenario: Выбор темы сохраняется
- **WHEN** в системе светлая тема, переключатель переведён в «тёмная», страница перезагружена
- **THEN** приложение в тёмной теме, в `localStorage` `graph-theme` = `dark`
## MODIFIED Requirements
### Requirement: Оболочка и адреса
Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
задач) и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать название, выпадающий список проектов
(«<метка> — <название>», текущий отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»), разделы
«Доска связей» и «Задачи» сегментным переключателем (текущий отмечен `aria-current="page"`), блок данных Jira с
действием «Обновить из Jira» (требование «Обновление из Jira»), переключатель темы (требование «Тема») и меню с
действием «Сменить токен». На узких экранах панель SHALL переноситься на две строки без горизонтальной прокрутки.
Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не пустым экраном.
#### Scenario: Переход между разделами
- **WHEN** на доске выбран раздел «Задачи»
- **THEN** адрес — `/app/issues` с текущим проектом, показан список задач; «назад» возвращает на доску
#### Scenario: Сменить токен
- **WHEN** в меню верхней панели выбрано «Сменить токен»
- **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 содержать заголовок «Доска связей», счётчики (задачи из всех, соседи по
связям, связи из всех, скрытые без связей), пометку источника, если на доске снимок из файла (с действием вернуться к
данным хаба), событие доски, кнопку «Проблемы» со счётчиком и меню с действиями «Перечитать», «Загрузить файл…»,
«Экспорт JSON», «Таблица связей», «Привязка к сетке» и «Сбросить раскладку» (с подтверждением). Под шапкой — панель
управления: мультиселекты «Спринты» и «Исполнители», переключатели группы «Показать» («Соседи по связям», «Только
передачи», «Скрыть без связей») и группы «Режим» («Критический путь», «По датам»), каждый с `aria-pressed` и прежней
подсказкой. Под панелью — легенда, которая сворачивается кнопкой «Легенда»; свёрнутая показывает число скрытых видов
работ и типов связей; состояние свёртки хранится в `localStorage` под ключом `jira-hub-legend`. «Обновить из Jira»,
тема и «Сменить токен» — в верхней панели приложения, на доске их нет.
У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку»,
открывающий `/app/issues/{key}` окном поверх доски; ключ карточки по-прежнему открывает задачу в Jira. Пока окно открыто
и после его закрытия доска SHALL сохранять прокрутку, раскладку, фильтры и строку состояния. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку
карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке
захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет.
Карточка задачи, статус Jira которой входит в `closed_statuses`, SHALL быть приглушена (полупрозрачна), под
указателем и с фокусом клавиатуры внутри карточки — в полную яркость; стрелка связи, у которой хотя бы один конец — такая задача, SHALL быть приглушена так же.
Приглушение — только вид: модель, видимая выборка, критический путь, проблемы, раскладка и экспорт JSON от него не
зависят; переключателя нет; правило одно для данных хаба, 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** адрес `/app/board`, доска показана с той же прокруткой и раскладкой, без повторной загрузки `GET /board`
#### Scenario: Возврат из другого раздела
- **WHEN** доска прокручена вниз, выбран раздел «Задачи», затем раздел «Доска связей»
- **THEN** доска показана с той же прокруткой
#### Scenario: Закрытые на доске приглушены
- **WHEN** на доске AURORA-1620 «В работе», AURORA-1621 «Готово», AURORA-1622 «Снято», связь AURORA-1620 → AURORA-1621
- **THEN** карточки AURORA-1621 и AURORA-1622 и стрелка AURORA-1620 → AURORA-1621 приглушены, AURORA-1620 — нет;
под указателем AURORA-1621 — в полную яркость; критический путь и ящик «Проблемы» те же, что без приглушения
#### Scenario: Свёрнутая легенда
- **WHEN** в легенде скрыт вид работы «тестирование», легенда свёрнута, страница перезагружена
- **THEN** легенда свёрнута, рядом с кнопкой «Легенда» — «скрыто: 1»; карточек вида «тестирование» на доске нет
### Requirement: Источник данных доски
Доска SHALL хранить явный источник данных — хаб или файл. Пока показан снимок из файла (CSV или JSON-снимок), ответы
хаба SHALL его не заменять; «Перечитать» SHALL возвращать источник «хаб» и загружать `GET /board` заново без обращения к
Jira. Импорт JSON в режиме хаба SHALL применять раскладку, дорожки и фильтры и перечитывать хаб. Шапка доски SHALL
показывать источник данных (пометка «из файла» с именем файла; данные хаба пометки не имеют) и одно текущее событие
доски (загрузка; хаб не отвечает — прежние данные, а при снимке из файла — «на доске снимок из файла»; ошибка разбора
файла — «файл не разобран: <причина>»; раскладка и фильтры из JSON применены; ошибка); новое событие заменяет
предыдущее целиком. События пакета обновления из Jira показывает верхняя панель приложения (требование «Обновление из
Jira»), не шапка доски.
#### Scenario: Поздний ответ хаба не затирает файл
- **WHEN** запрос `GET /board` в пути, и в это время загружен CSV
- **THEN** после ответа хаба на доске по-прежнему данные CSV с пометкой «из файла»
#### Scenario: Пакет завершился при снимке из файла
- **WHEN** на доске снимок из CSV и завершился пакет обновления из Jira
- **THEN** на доске по-прежнему данные CSV с пометкой «из файла»; после «Перечитать» — данные хаба
#### Scenario: Хаб недоступен при снимке из файла
- **WHEN** хаб не отвечает и загружен JSON-снимок доски
- **THEN** шапка показывает «хаб не отвечает — на доске снимок из файла», а не «показаны прежние данные»
#### Scenario: Нет второго полного прогона
- **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, доска перерисована (фильтр, «Перечитать»)
- **THEN** кнопка «Обновить из Jira» в верхней панели остаётся недоступной, в хабе один пакет, его опрашивает один опрос
#### Scenario: Пакет пропал
- **WHEN** опрос пакета получил 404, на доске данные хаба
- **THEN** доска показывает прежние данные без своего события об ошибке; сообщение о пропавшем пакете — в верхней панели
#### Scenario: Пакет после «Перечитать»
- **WHEN** пакет `viewer` поставлен с другой вкладки и не завершён, на доске нажато «Перечитать»
- **THEN** верхняя панель показывает «пакет #N выполняется», по завершении доска перечитывает данные
#### Scenario: «Перечитать» во время пакета
- **WHEN** пакет `viewer` выполняется и на доске нажато «Перечитать»
- **THEN** верхняя панель по-прежнему показывает «пакет #N выполняется», шапка доски — без события пакета
#### Scenario: Чужой пакет не подхватывается
- **WHEN** незавершённый пакет автора `viewer` состоит из команд `ensure_fields`, открыта доска
- **THEN** доска и верхняя панель его не опрашивают, кнопка «Обновить из Jira» доступна
### Requirement: Список задач
Страница `/app/issues` SHALL показывать задачи текущего проекта из `GET /issues?project=<текущий>` таблицей: ключ
(открывает карточку задачи окном поверх списка) с пометкой «сосед» (относительно проекта), тема, статус Jira,
исполнитель, срок (`planned_end`, иначе `duedate`; просроченный срок незакрытой задачи выделен), спринт, эпик (имя
эпика, иначе ключ), вид работы, тип; статуса по факту в списке нет. Щелчок по строке (кроме ссылок и выделения текста)
SHALL открывать карточку задачи так же, как ключ. Шапка страницы SHALL показывать число показанных задач.
Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид работы, статус Jira, метка, эпик,
только соседи, поиск по ключу или теме, скрыть закрытые), храниться в query-строке адреса вместе с `project` и
применяться сервером. На виду SHALL быть поиск, переключатель «Открытые | Все», переключатель «Только соседи» и
кнопка «Фильтры» с числом выбранных выпадающих списков; шесть выпадающих списков (спринт, исполнитель, вид работы,
статус Jira, метка, эпик) — в панели, которая раскрывается этой кнопкой; каждый выбранный список SHALL показываться
снимаемой плашкой «<подпись>: <значение>» и под строкой фильтров должно быть действие «Сбросить», снимающее все шесть
списков. Варианты выпадающих списков SHALL браться из снимка `GET /board?project=<текущий>`; значение фильтра из
адреса, которого нет среди вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Положение «Открытые»
SHALL быть по умолчанию (в адресе ничего), «Все» — `closed=1` в адресе; «Открытые» — запрос с `hide_closed=1`,
кроме случая, когда в фильтре «Статус Jira» выбран статус из `closed_statuses`. Параметр `fact_status` в адресе
игнорируется. Задачи SHALL загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя
порция полная.
#### Scenario: Фильтр в адресе
- **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель
- **THEN** адрес содержит `project=`, `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого
адреса в новой вкладке показывает ту же выборку, кнопка «Фильтры» показывает 2, видны плашки спринта и исполнителя
#### Scenario: Снять фильтр плашкой
- **WHEN** выбраны спринт и исполнитель, нажат ✕ на плашке спринта
- **THEN** в адресе нет `sprint=`, `assignee=` остался, таблица перезапрошена
#### Scenario: Конец списка
- **WHEN** вторая порция вернула 17 задач
- **THEN** в таблице 67 задач, «Показать ещё» не показана
#### Scenario: Фильтр по закрытому спринту
- **WHEN** открыт адрес `/app/issues?project=AWPCA&sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет
- **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру
#### Scenario: Карточка поверх списка
- **WHEN** список отфильтрован по спринту, прокручен и догружен «Показать ещё», открыта карточка задачи и окно закрыто
- **THEN** адрес списка с тем же фильтром, в таблице те же строки, прокрутка та же
#### Scenario: Щелчок по строке
- **WHEN** в таблице щёлкнута ячейка темы строки AURORA-1620
- **THEN** адрес `/app/issues/AURORA-1620`, карточка открыта окном поверх списка
#### Scenario: Закрытые скрыты по умолчанию
- **WHEN** открыт `/app/issues`, в зеркале задачи в статусах «В работе», «Готово», «Снято»
- **THEN** выбрано «Открытые», запрос `GET /issues` с `hide_closed=1`, в таблице только «В работе»; после выбора «Все»
адрес содержит `closed=1`, в таблице все три
#### Scenario: Выбран закрытый статус
- **WHEN** выбрано «Открытые» и в «Статус Jira» выбрано «Готово»
- **THEN** запрос `GET /issues?status=Готово` без `hide_closed`, в таблице задачи в статусе «Готово»
### Requirement: Проект в приложении
Доска и список задач SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром `project` в
адресе `/app/board` и `/app/issues`; без параметра — последний выбранный в этом браузере (`localStorage`), если он есть
в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории.
Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и выпадающий список
проектов вместо данных. Проектов нет — доска и список SHALL показывать приглашение создать проект («+ Новый проект…»).
Переходы «Доска связей» / «Задачи» SHALL сохранять текущий проект. Карточка задачи (`/app/issues/{key}`) от проекта не зависит.
Доска SHALL загружать `GET /board?project=<текущий>`; «Обновить из Jira» — `POST /refresh {full: true}`, обновляет все
проекты. Раскладка, дорожки и фильтры доски в `localStorage` SHALL храниться отдельно для каждого проекта;
у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов, сохраняется). Фильтр спринтов SHALL
показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`) есть спринт; иначе — «колонки
«Спринт» нет — показаны все задачи».
Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка,
название, номер доски Jira (необязательно; подсказка «scrum или kanban определится сам»). «Создать» — `POST /projects`
с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с кнопкой, окно остаётся открытым; успех — окно
закрывается, текущим становится новый проект, верхняя панель показывает ход пакета обновления из ответа (`batch_id`).
#### Scenario: Первый вход после выкатки
- **WHEN** открыт `/app/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK`
- **THEN** адрес — `/app/board?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек
#### Scenario: Переключение проекта
- **WHEN** на доске `AWPCA` в выпадающем списке выбран `STOCKS_DRRK`, затем открыт раздел «Задачи»
- **THEN** доска — `GET /board?project=STOCKS_DRRK` со своей раскладкой; адрес «Задач» — `/app/issues?project=STOCKS_DRRK`
#### Scenario: Неизвестный проект в адресе
- **WHEN** открыт `/app/issues?project=NOPE`
- **THEN** «проект NOPE не найден» и список проектов; запроса `GET /issues` нет
#### Scenario: Создание проекта из формы
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», доска пусто; нажато «Создать»
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", author: "viewer"}`; текущий проект — `STOCKS_DRRK`;
верхняя панель показывает «пакет #N выполняется»
#### Scenario: Метка занята
- **WHEN** в форме введена метка `awpca`, хаб ответил 409
- **THEN** окно открыто, рядом с «Создать» — текст ошибки хаба
#### Scenario: Канбан-проект
- **WHEN** текущий проект — `STOCKS_DRRK`, ни у одной своей задачи снимка нет спринта (у соседа из `AWPCA` спринт есть)
- **THEN** вместо фильтра спринтов — «колонки «Спринт» нет — показаны все задачи»
### Requirement: Сборка и разработка
Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и
`package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в
`src/jira_hub/web/app/`; каталог сборки не хранится в git. Шрифты (Mulish — текст интерфейса, IBM Plex Mono — ключи
задач и исходники), иконки и прочие ресурсы страницы 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** страница не запрашивает ресурсов вне хаба, текст набран Mulish, ключи задач — IBM Plex Mono