Compare commits
2 Commits
57bf1faf7a
...
aad35177bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aad35177bf | ||
|
|
fa20059ecd |
467
docs/superpowers/plans/2026-10-21-redesign.md
Normal file
467
docs/superpowers/plans/2026-10-21-redesign.md
Normal 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)`
|
||||
2
openspec/changes/redesign/.openspec.yaml
Normal file
2
openspec/changes/redesign/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-21
|
||||
189
openspec/changes/redesign/design.md
Normal file
189
openspec/changes/redesign/design.md
Normal 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) | вид будет меняться по замечаниям, эталоны мешали бы | низкая |
|
||||
40
openspec/changes/redesign/proposal.md
Normal file
40
openspec/changes/redesign/proposal.md
Normal 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-тесты не трогаются).
|
||||
- Мобильная вёрстка доски сверх нынешней (узкий экран — только без поломок).
|
||||
320
openspec/changes/redesign/specs/web-app/spec.md
Normal file
320
openspec/changes/redesign/specs/web-app/spec.md
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user