## MODIFIED Requirements ### Requirement: Вход по токену Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в `localStorage` под ключом `jira-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса. Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с подсказкой `uv run jira-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода. Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен» SHALL очищать токен и показывать экран входа. Ответ 401 SHALL выводить на экран входа, только если запрос ушёл с текущим токеном: запоздавший 401 на запрос с прежним токеном после входа с новым не учитывается. Если проверка токена получила ответ 4xx, кроме 401, экран входа SHALL показывать текст ошибки хаба, а не «хаб не отвечает». #### Scenario: Первый вход - **WHEN** приложение открыто впервые, токена нет - **THEN** показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён #### Scenario: Устаревший токен - **WHEN** сохранённый токен не подходит и запрос ответил 401 - **THEN** показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны #### Scenario: Токен от прежней доски - **WHEN** в `localStorage` есть `jira-hub-token`, сохранённый страницей `links.html` - **THEN** приложение входит с ним без запроса токена #### Scenario: Запоздавший 401 от прежнего токена - **WHEN** запрос ушёл с прежним токеном, пользователь вошёл с новым, и после этого запрос ответил 401 - **THEN** приложение остаётся на странице, новый токен сохранён #### Scenario: Хаб отверг проверку токена - **WHEN** проверка `GET /settings` ответила 400 с `detail` - **THEN** экран входа показывает текст `detail`, а не «хаб не отвечает» ### Requirement: Доска связей Страница `/app/board` SHALL воспроизводить поведение доски связей хаба (`links.html` ревизии 3931a88) без изменений, кроме перечисленных в этом требовании и в требовании «Источник данных доски»: построение модели из `GET /board`, CSV фильтра Jira и JSON доски (`arm-ka-links/1`); дорожки по эпикам со свёрткой и перестановкой, служебные дорожки внизу; карточки с перетаскиванием, растяжкой, размером по содержимому и привязкой к сетке; стрелки связей с выравниванием; фильтры по спринтам и исполнителям; легенда со скрытием видов работ и типов связей; «+ соседи по связям», «⇄ передачи», «скрыть без связей», «⚑ критический путь», «▥ по датам» с колонками дат и их растяжкой; ящик «Проблемы»; таблица связей; экспорт и импорт JSON; тема; «Сбросить раскладку». Раскладка, дорожки, фильтры и тема SHALL храниться в `localStorage` под теми же ключами `graph-<имя>…`, что у `links.html`, с той же миграцией старых ключей. Результат построения модели, видимой выборки, критического пути и проблем SHALL совпадать с результатом `links.html` на одних и тех же данных и фильтрах. У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку», ведущий на `/app/issues/{key}`; ключ карточки по-прежнему открывает задачу в Jira. Действия «Сменить токен» в меню доски нет — оно в верхней панели приложения. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет. При возврате на `/app/board` с другой страницы приложения в той же вкладке доска SHALL восстанавливать прежнюю прокрутку; после перезагрузки страницы доска открывается сверху. #### Scenario: Раскладка переживает переход - **WHEN** раскладка карточек сохранена страницей `links.html`, открыта `/app/board` с теми же данными - **THEN** карточки стоят на тех же местах, дорожки в том же порядке и свёртке, фильтры те же #### Scenario: Совпадение со старой доской - **WHEN** на одной фикстуре `GET /board` и одном наборе фильтров построены модель, видимая выборка, критический путь и проблемы старой страницей и приложением - **THEN** результаты совпадают #### Scenario: Открыть карточку - **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку» - **THEN** открыта `/app/issues/AURORA-1620` внутри приложения #### Scenario: Возврат с карточки - **WHEN** доска прокручена вниз, нажат значок «Открыть карточку», затем выбран раздел «Доска» - **THEN** доска показана с той же прокруткой ### Requirement: Источник данных доски Доска SHALL хранить явный источник данных — хаб или файл. Пока показан снимок из файла (CSV или JSON-снимок), ответы хаба SHALL его не заменять; «Перечитать» SHALL возвращать источник «хаб» и загружать `GET /board` заново без обращения к Jira. Импорт JSON в режиме хаба SHALL применять раскладку, дорожки и фильтры и перечитывать хаб. «Обновить из Jira» SHALL проверять агента (`GET /agent`; офлайн — предупреждение, пакет всё равно ставится), ставить `POST /refresh {full: true, author: "viewer"}`, опрашивать `GET /batches/{id}` каждые 3 с одним опросом до терминального статуса пакета и затем перечитывать `GET /board`. Кнопка «Обновить из Jira» SHALL быть недоступна, пока запрос `/refresh` в пути или пакет выполняется. Незавершённый пакет `refresh` с автором `viewer` SHALL подхватываться при каждой успешной загрузке из хаба; пакеты `viewer` с другими командами не подхватываются. Опрос пакета SHALL прекращаться при ответе 404 и после 5 ошибок «хаб не отвечает» подряд, с сообщением и снова доступной кнопкой. Шапка доски SHALL показывать источник данных (данные хаба на время последнего полного прогона; «из файла») и одно текущее событие (загрузка; пакет выполняется / выполнен / ошибка с текстом; хаб не отвечает — прежние данные, а при снимке из файла — «на доске снимок из файла»; агент офлайн — пакет поставлен; ошибка разбора файла — «файл не разобран: <причина>»); новое событие заменяет предыдущее целиком. Пока пакет выполняется, «Перечитать» и загрузка файла SHALL не стирать событие пакета. #### Scenario: Поздний ответ хаба не затирает файл - **WHEN** запрос `GET /board` в пути, и в это время загружен CSV - **THEN** после ответа хаба на доске по-прежнему данные CSV с пометкой «из файла» #### Scenario: Нет второго полного прогона - **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, доска перерисована (фильтр, «Перечитать») - **THEN** кнопка остаётся недоступной, в хабе один пакет, его опрашивает один опрос #### Scenario: Пакет пропал - **WHEN** опрос пакета получил 404 - **THEN** опрос остановлен, показано сообщение, кнопка «Обновить из Jira» снова доступна #### Scenario: Пакет после «Перечитать» - **WHEN** пакет `viewer` поставлен с другой вкладки и не завершён, нажато «Перечитать» - **THEN** доска показывает «пакет #N выполняется» и по завершении перечитывает данные #### Scenario: «Перечитать» во время пакета - **WHEN** пакет `viewer` выполняется и нажато «Перечитать» - **THEN** шапка по-прежнему показывает «пакет #N выполняется» #### Scenario: Хаб недоступен при снимке из файла - **WHEN** хаб не отвечает и загружен JSON-снимок доски, либо на доске снимок из CSV и хаб не ответил на перечитывание снимка после завершения пакета - **THEN** шапка показывает «хаб не отвечает — на доске снимок из файла», а не «показаны прежние данные» #### Scenario: Чужой пакет не подхватывается - **WHEN** незавершённый пакет автора `viewer` состоит из команд `ensure_fields` - **THEN** доска его не опрашивает, кнопка «Обновить из Jira» доступна ### Requirement: Список задач Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (ссылка на карточку), тема, тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы, статус по факту, признак «сосед». Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид работы, статус по факту, статус Jira, метка, эпик, только соседи, поиск по ключу или теме), храниться в query-строке адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`; значение фильтра из адреса, которого нет среди вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Задачи SHALL загружаться порциями по 50 строк; «Показать ещё» SHALL быть доступна, пока последняя порция полная. #### Scenario: Фильтр в адресе - **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель - **THEN** адрес содержит `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого адреса в новой вкладке показывает ту же выборку #### Scenario: Конец списка - **WHEN** вторая порция вернула 17 задач - **THEN** в таблице 67 задач, «Показать ещё» не показана #### Scenario: Фильтр по закрытому спринту - **WHEN** открыт адрес `/app/issues?sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет - **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру ### Requirement: Карточка задачи Страница `/app/issues/{key}` SHALL показывать задачу из `GET /issues/{key}`: поля зеркала, ссылку на задачу в Jira, описание как текст с сохранёнными переносами строк (без разбора разметки Jira), исходящие и входящие связи с подписью типа (логический тип из `link_types`, иначе имя типа Jira) и ссылками на карточки связанных задач, 10 последних команд по задаче (id, пакет, тип, статус, время завершения, ошибка), а если команд не было — текст «команд по задаче не было». Если задачи нет в зеркале (404), страница SHALL сообщать об этом со ссылкой на список. #### Scenario: Связанная задача - **WHEN** у AURORA-1620 исходящая связь `ASUPR_Блокер_Начала` на AURORA-1621 - **THEN** в карточке «блокирует начало → AURORA-1621», ключ ведёт на `/app/issues/AURORA-1621` #### Scenario: Задача без команд - **WHEN** по AURORA-1620 не было команд - **THEN** в разделе «Последние команды» — текст «команд по задаче не было», таблицы нет ### Requirement: Сборка и разработка Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и `package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в `src/jira_hub/web/app/`; каталог сборки не хранится в git. Шрифты и прочие ресурсы страницы SHALL входить в сборку: приложение не обращается к внешним адресам. `make check` SHALL запускать `uv run pytest`, проверку типов и линтер фронта и тесты фронта; зелёным считается только весь набор. `npm run dev` SHALL поднимать сервер разработки с прокси маршрутов API на хаб из `JIRA_HUB_URL` (по умолчанию тестовый `http://127.0.0.1:8791`). #### Scenario: Сборка с нуля - **WHEN** в чистом checkout выполнен `make web` - **THEN** в `src/jira_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки #### Scenario: Без интернета - **WHEN** приложение открыто на машине без доступа в интернет - **THEN** страница не запрашивает ресурсов вне хаба, текст набран IBM Plex Sans