Спека и план: боковая панель, доска без чередования дорожек (#69)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-22 08:47:43 +04:00
parent 7911dc71d8
commit c20e2bf14b
5 changed files with 430 additions and 0 deletions

View File

@ -0,0 +1,65 @@
# Боковая панель и доска без чередования — план
> **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:** заменить верхнюю панель приложения закреплённой боковой (со свёрткой и выезжающим вариантом на узком экране), убрать чередование дорожек доски и показать просрочку на карточке доски.
**Architecture:** `Layout` рисует grid `.shell` из `Sidebar` и `main.page`; прокрутка остаётся у окна. `Sidebar` собирает существующие `ProjectPicker`, `DataStatus`, `ThemeSwitch`, `Menu`; режим (развёрнута / свёрнута / узкий экран) — локальное состояние + `localStorage` + `matchMedia`. Доска: CSS и проп `today` у `Card`.
**Tech Stack:** React 19 + TS, Vitest + Testing Library, Playwright.
**Spec:** `openspec/changes/sidebar/` (proposal, design, specs/web-app/spec.md)
## Global Constraints
- Литералы цвета — только в `web/src/styles/tokens.css` (тест `tokens.test.ts`).
- Ключ свёртки `jira-hub-sidebar`, значение `collapsed`; без записи — развёрнута; доступ к `localStorage` в `try/catch`.
- Ширины: развёрнутая 240 px, свёрнутая 64 px; узкий экран — `(max-width: 639px)`, строка 56 px, выезжающая панель `min(300px, 85vw)`.
- Подписи: «Хаб Jira», «Свернуть панель», «Развернуть панель», «Меню», «Разделы», «Доска связей», «Задачи», «Обновить из Jira», «просрочено».
- Модель доски, golden-эталоны, API, ключи `graph-*` не меняются.
## Review Focus
- Прокрутка доски у окна: восстановление прокрутки, липкая шапка, полоса дат, ящик «Проблемы», `.hscroll` — после смены каркаса (e2e `board.spec` сценарии прокрутки).
- Окно карточки и окна диалогов поверх панели (z-index), фокус возвращается после закрытия.
- Узкий экран: выезжающая панель не оставляет горизонтальной прокрутки и закрывается при переходе по ссылке.
- Свёрнутая панель: «Обновить из Jira» показывает ход пакета (значок крутится, текст события в `title`/`role=status`).
- Просрочка: граница — срок сегодня не просрочен; закрытая задача не просрочена.
---
### Task 1: Доска — без чередования, просрочка на карточке
**Files:** `web/src/board/board.css`, `web/src/styles/tokens.css`, `web/src/styles/tokens.test.ts`, `web/src/board/components/Card.tsx`, `web/src/board/components/BoardCanvas.tsx` (передача `today`), `web/src/board/BoardPage.tsx`, `web/src/styles/base.css` (`.visually-hidden`, если нет), тест `web/src/board/components/Card.test.tsx`.
- [ ] Тест `Card`: `today="2026-10-22"`, `due="2026-10-20"`, не закрыта → `.due.late`, `title="просрочено"`, текст «просрочено» в DOM; `closed` → без `late`; `due="2026-10-22"` → без `late`.
- [ ] Тест токенов: в `tokens.css` нет `--lane-alt`.
- [ ] Реализация (design 9, 10); `cardPropsEqual` сравнивает `today`.
- [ ] `npx vitest run` зелёный, коммит.
### Task 2: Боковая панель (широкий экран)
**Files:** create `web/src/shell/Sidebar.tsx`, `web/src/shell/sidebarPref.ts`, `web/src/shell/Sidebar.test.tsx`; modify `Layout.tsx`, `DataStatus.tsx` (проп `compact`), `shell.css`, `projects/projects.css`, `ui/Menu.tsx` (`up`), `ui/ui.css`, `ui/icons.tsx`; delete `AppBar.tsx`, `AppBar.test.tsx` (перенести его проверки в `Sidebar.test.tsx`).
**Interfaces:** `readSidebar(): boolean` (true — свёрнута), `saveSidebar(collapsed: boolean): void`; `Sidebar({ section, project }: { section: Section | null; project: string | null })`; `Section` переезжает в `Sidebar.tsx`.
- [ ] Тесты: ссылки «Доска связей»/«Задачи» с `aria-current`; «Свернуть панель» → `localStorage['jira-hub-sidebar']==='collapsed'`, ссылки с `aria-label`, повторный рендер — свёрнута; «Сменить токен» очищает токен; в свёрнутой — кнопка «Обновить из Jira» и `role=status`.
- [ ] Реализация (design 1–3, 6–8).
- [ ] `npx vitest run` и `npm run lint` зелёные, коммит.
### Task 3: Узкий экран
**Files:** `Sidebar.tsx`, `Layout.tsx`, `shell.css`, `board/board.css` (`--topbar-h` у `.drawer`), `Sidebar.test.tsx`.
- [ ] Тесты (matchMedia замокан на узкий): видна кнопка «Меню», панели нет; «Меню» → панель открыта (`aria-expanded=true`), фокус на первой ссылке; Escape → закрыта, фокус на «Меню»; щелчок по разделу → закрыта.
- [ ] Реализация (design 4, 5).
- [ ] vitest зелёный, коммит.
### Task 4: e2e, снимки, документация
**Files:** `web/e2e/*.spec.ts`, `web/e2e/hub.ts`, `web/e2e/screens.spec.ts`, `ARCHITECTURE.md`.
- [ ] Поправить селекторы `.appbar`/сегментов на панель; новый e2e: после прокрутки доски вниз ссылка «Задачи» в панели видна в окне (`toBeInViewport`); свёртка переживает перезагрузку; на 390 «Меню» → «Задачи» → список, `scrollWidth <= innerWidth`.
- [ ] `screens.spec`: добавить снимки `board-collapsed` (1920) и `menu` (390).
- [ ] ARCHITECTURE.md: оболочка — боковая панель.
- [ ] `make check`, `npm run e2e`, `SCREENS=1 npx playwright test e2e/screens.spec.ts` зелёные; коммит, push.

View File

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

View File

@ -0,0 +1,85 @@
## Context
`main` 7911dc7 (после #66/#67). Оболочка: `web/src/shell/Layout.tsx` рисует `AppBar` (`shell/AppBar.tsx`: название,
`ProjectPicker`, `Segmented` разделов со ссылками, `DataStatus`, `ThemeSwitch`, `Menu` «Ещё» со «Сменить токен») и
`<main className="page …">`. Прокрутка страниц — у окна (`window.scrollY`): на этом держатся восстановление прокрутки
доски (`BoardCanvas`), липкая шапка доски (`.sticky`, `top: 0`, высота — в `--head-h`), липкая полоса дат, ящик
«Проблемы» и полоса горизонтальной прокрутки `.hscroll` (`bottom: 0`). Верхняя панель не липкая (решение 15 #65),
поэтому уезжает.
Доска: `.lane:nth-child(even of .lane){background:var(--lane-alt)}`; колонки дат `.dcol` (`.past` — 5% `--p-crit`,
`.today` — 7% действия) рисуются в DOM до дорожек, поэтому непрозрачный фон чётной дорожки закрывает их подсветку.
Карточка (`board/components/Card.tsx`) показывает «до {дата}» без признака просрочки; признак есть только в карточке
задачи (`issues/card.ts` `isOverdue`, бейдж «просрочено») и в списке (красная дата).
## Goals / Non-Goals
**Goals:** навигация и «Обновить из Jira» всегда на экране; доска шире не теряет (свёртка); просрочка видна на доске.
**Non-Goals:** см. proposal.
## Decisions
1. **Каркас.** `Layout` рисует `<div class="shell">` с `Sidebar` и `<main class="page …">`. `.shell` — grid
`grid-template-columns: var(--sidebar-w) minmax(0, 1fr)`; панель `position: sticky; top: 0; height: 100dvh;
overflow-y: auto`. Прокрутка остаётся у окна — восстановление прокрутки, липкие шапка доски, полоса дат, ящик и
`.hscroll` работают без изменений. Ширина: развёрнутая 240 px, свёрнутая 64 px (`--sidebar-w`).
2. **Состав развёрнутой панели** (сверху вниз): строка «Хаб Jira» + кнопка-иконка «Свернуть панель»
(`aria-expanded`); `ProjectPicker` на всю ширину; `<nav aria-label="Разделы">` — ссылки «Доска связей» и «Задачи»
со значками, высота 40 px, текущая — фон `--color-bg-selected`, текст действия, `aria-current="page"`; разделитель
с автоматическим отступом; блок данных `DataStatus` в колонку (агент, «данные на …», кнопка «Обновить из Jira» на
всю ширину, строка события под кнопкой с переносом — абсолютное позиционирование события из R-7 #65 больше не
нужно); строка: `ThemeSwitch` слева, `Menu` «Ещё» справа, меню открывается вверх.
3. **Свёрнутая панель.** Только значки: кнопка «Развернуть панель», метка текущего проекта мелким текстом (до 6
символов, полный текст в `title`; щелчок разворачивает панель), значки разделов (`aria-label` = название раздела,
`title`), «Обновить из Jira» — `IconButton` с крутящимся значком при пакете и точкой агента в углу, событие пакета —
в `title` и в скрытом `role="status"`, тема, меню. Ключ `localStorage` `jira-hub-sidebar`: `collapsed` — свёрнута,
иначе развёрнута; чтение и запись — в `try/catch`.
4. **Узкий экран (<640 px).** Панели в потоке нет: сверху липкая строка 56 px — кнопка «Меню» (`IconMenu`,
`aria-expanded`), «Хаб Jira», справа `IconButton` «Обновить из Jira». «Меню» открывает ту же развёрнутую панель
поверх страницы (фиксирована слева, ширина `min(300px, 85vw)`, подложка `--color-overlay`); закрывается выбором
раздела, Escape, щелчком по подложке; фокус при открытии — на первую ссылку раздела, при закрытии — на «Меню».
Свёртка на узком экране не действует. Режим определяется `matchMedia("(max-width: 639px)")` с подпиской на смену.
5. **Шапка доски** остаётся липкой с `top: 0` (теперь это верх окна); правило `@media (max-width:640px){.sticky{position:static}}`
не трогаем, только `top` у `.sticky`, `.dates-bar` и `.drawer` на узком экране сдвигается на высоту строки 56 px
через `--topbar-h` (0 на широких).
6. **Меню вверх.** `Menu` получает проп `up?: boolean` → класс `dd--up` (`.dd--up > .dd-menu { top: auto; bottom: calc(100% + 4px) }`).
7. **Иконки.** Новые в `ui/icons.tsx`: `IconBoard` (граф из трёх узлов), `IconList` (строки), `IconMenu` (три
линии), `IconSidebar` (прямоугольник с полосой слева).
8. **`AppBar` удаляется**, `Segmented` больше не нужен оболочке (остаётся в `ui` для списка задач). `.appbar …` в
`shell.css` и `projects.css` заменяются стилями `.sidebar`.
9. **Дорожки без чередования.** Правило `.lane:nth-child(even of .lane)` и токен `--lane-alt` удаляются в обеих темах;
развёрнутые дорожки прозрачны (фон — поверхность `.wrap`), разделитель — верхняя граница; свёрнутая дорожка
остаётся на `--color-bg-track`.
10. **Просрочка на карточке доски.** `Card` получает проп `today: string` (ISO, из `BoardPage`, где он уже есть для
`computeVisible`) и считает `late = !closed && !!n.due && n.due < today`; у `.due` класс `late` — цвет
`--color-status-error`, `title="просрочено"`, скрытый текст «, просрочено» (`.visually-hidden`). Карточка в слое
измерения тоже получает `today`, размер не меняется (скрытый текст вне потока). Сравнение в `cardPropsEqual`
дополняется `today`. Модель доски и эталоны не меняются.
11. **Тесты.** Vitest: `Sidebar` (разделы и `aria-current`, свёртка и ключ `jira-hub-sidebar`, «Сменить токен»,
узкий режим: «Меню» открывает и Escape закрывает, выбор раздела закрывает), `Card` (просрочка: незакрытая с
прошедшим сроком — `late`, закрытая и будущая — нет), тест токенов (нет `--lane-alt`). Playwright: существующие
сценарии с новыми селекторами; новый — панель видна после прокрутки доски вниз и свёртка переживает перезагрузку;
`screens.spec` — снимки свёрнутой панели и открытого меню на 390.
## Risks / Trade-offs
- [Панель 240 px сужает доску на ноутбуке] → свёртка в 64 px, выбор запоминается.
- [Две копии меню на узком экране (строка и панель)] → нет: на узком экране панель одна, она же выезжает; в строке
только «Меню» и «Обновить из Jira».
- [e2e и юнит-тесты, завязанные на `.appbar`] → правятся в той же задаче, что и разметка.
## Decisions made without discussion
Владелец 22.10.2026: «в графе связей идёт чередование строк по цвету. оно мешает. его или сделать менее заметным или
убрать вообще, тк не видно просроченные задачи, и закрепить хэдер, а лучше сделать его сайдбаром». Ранее (#65)
отдал UX на усмотрение агента. Всё ниже — решения агента.
| # | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 22.10.2026 | Боковая панель, а не липкая верхняя (решения 1–2) | владелец предпочёл «лучше сайдбар»; вертикальный список разделов растёт без переноса строк | средняя: вернуть верхнюю панель — новая вёрстка оболочки |
| 2 | 22.10.2026 | Чередование убрано совсем, не ослаблено (решение 9) | любой непрозрачный фон дорожки снова закроет колонки дат; разделителя-линии достаточно | низкая: вернуть правило и токен |
| 3 | 22.10.2026 | Просрочка на карточке доски цветом ошибки + «просрочено» в подсказке (решение 10) | «не видно просроченные» — вне режима «по датам» просрочку на доске не было видно вовсе | низкая: класс и проп |
| 4 | 22.10.2026 | Свёртка панели в полосу 64 px с запоминанием (решение 3) | доске нужна ширина; раз в спринт граф — тогда удобно свернуть | низкая |
| 5 | 22.10.2026 | На узком экране — строка с «Меню» и выезжающая панель (решение 4) | закреплённая панель 4–5 строк (M-12 #65) съела бы экран | низкая: узкий экран — только «без поломок» |
| 6 | 22.10.2026 | Развёрнута по умолчанию, ширина 240 px | на 1920 место есть; название проекта видно целиком | низкая |

View File

@ -0,0 +1,24 @@
## Why
После редизайна (#65) владелец 22.10.2026 оставил два замечания (jira-hub#69):
- на доске связей дорожки чередуются по цвету фона; это мешает и прячет просроченные задачи — подсветка прошедших
дат в режиме «по датам» лежит под фоном каждой второй дорожки, а на самой карточке просрочка ничем не отмечена;
- верхняя панель приложения уезжает при прокрутке — «закрепить хэдер, а лучше сделать его сайдбаром».
## What Changes
- **Боковая панель вместо верхней:** название, проект, разделы вертикальным списком со значками, внизу — блок данных
Jira с «Обновить из Jira», тема и меню «Ещё». Панель закреплена у левого края на всю высоту окна и не прокручивается
со страницей; сворачивается в узкую полосу со значками (выбор хранится в `localStorage`). На экранах уже 640 px —
закреплённая строка с кнопкой «Меню», которая открывает панель поверх страницы.
- **Доска без чередования дорожек:** фон развёрнутых дорожек один, дорожки разделяются линией; подсветка колонок «по
датам» видна на всех дорожках.
- **Просрочка на карточке доски:** у незакрытой задачи с прошедшим сроком дата «до …» — цветом ошибки, с пометкой
«просрочено» для подсказки и экранного чтения.
## Non-Goals
- Изменения состава действий оболочки, логики обновления из Jira, адресов и хранилища доски.
- Новые разделы в навигации.
- Изменения API хаба, MCP, агента; модели доски и golden-эталонов.

View File

@ -0,0 +1,254 @@
## MODIFIED Requirements
### Requirement: Оболочка и адреса
Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
задач) и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
на страницу «не найдено» со ссылками на разделы. Боковая панель SHALL быть закреплена у левого края окна на всю его
высоту и MUST NOT прокручиваться вместе со страницей; страница занимает остальную ширину. Сверху вниз панель SHALL
содержать: название «Хаб Jira» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий
отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»); разделы «Доска связей» и «Задачи» — вертикальным
списком ссылок со значками (текущий отмечен `aria-current="page"`); внизу — блок данных Jira с действием «Обновить из
Jira» (требование «Обновление из Jira»), переключатель темы (требование «Тема») и меню с действием «Сменить токен».
Свёрнутая панель SHALL быть узкой полосой с теми же действиями значками: «Развернуть панель», метка текущего проекта,
разделы, «Обновить из Jira» с точкой состояния агента, тема, меню; названия — в подсказках и `aria-label`. Свёртка
SHALL храниться в `localStorage` под ключом `jira-hub-sidebar` (`collapsed`; без записи — развёрнута). На экранах уже
640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», названием и кнопкой «Обновить из
Jira»; «Меню» открывает развёрнутую панель поверх страницы, панель закрывается выбором раздела, клавишей Escape или
щелчком мимо неё. Горизонтальной прокрутки страницы на любой ширине нет.
Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не пустым экраном.
#### Scenario: Переход между разделами
- **WHEN** на доске в боковой панели выбран раздел «Задачи»
- **THEN** адрес — `/app/issues` с текущим проектом, показан список задач; «назад» возвращает на доску
#### Scenario: Сменить токен
- **WHEN** в меню боковой панели выбрано «Сменить токен»
- **THEN** токен очищен, показан экран входа
#### Scenario: Панель на месте при прокрутке
- **WHEN** доска прокручена до самого низа
- **THEN** боковая панель видна целиком на прежнем месте: разделы и «Обновить из Jira» доступны без прокрутки вверх
#### Scenario: Свёрнутая панель
- **WHEN** нажата «Свернуть панель», страница перезагружена
- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; «Развернуть панель»
возвращает полную панель
#### Scenario: Узкий экран
- **WHEN** ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи»
- **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 восстанавливать прежнюю
прокрутку; после перезагрузки страницы доска открывается сверху.
Дорожки SHALL разделяться линией; фон развёрнутых дорожек один, без чередования, чтобы подсветка колонок «по датам»
(прошедшие даты, сегодня) была видна на всех дорожках; свёрнутая дорожка выделяется фоном. У карточки задачи, статус
Jira которой не входит в `closed_statuses`, с плановой датой окончания раньше сегодняшней дата «до …» 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»; карточек вида «тестирование» на доске нет
#### Scenario: Просрочка на доске
- **WHEN** сегодня 22.10, у AURORA-1620 статус «В работе» и срок 20.10, у AURORA-1621 «Готово» и срок 20.10, включено
«По датам»
- **THEN** дата AURORA-1620 — цветом ошибки с пометкой «просрочено», у AURORA-1621 — обычная; подсветка колонки 20.10
видна на всех дорожках, дорожки одного фона
### 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: Проект в приложении
Доска и список задач 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: Обновление из 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`