## Context Просмотрщик «Спринт 2 линк» — один HTML-файл (~117 КБ, из них ~19 КБ — встроенная выгрузка CSV от 11.10.2026) без зависимостей: разбор CSV фильтра Jira (`fromJiraCsv`) → модель `{nodes, links, sprints}` → доска (дорожки по эпикам, карточки, стрелки блокировок, режим «по датам», критический путь, панель проблем, таблица), раскладка и фильтры — в `localStorage` под ключами `graph-<имя>:board`, не зависящими от источника; экспорт/импорт JSON (`arm-ka-links/1`). Модель узла: `{key, title, kind, prio, due, status, epic, sprint, who}`; связи `{src, dst, type}` с логическими типами «блокирует начало / блокирует завершение / связанные» (`LINK_TYPES` переводит имена Jira `ASUPR_*`). Хаб (`main` fc732eb): зеркало `issue` содержит все нужные поля (`planned_end`, `epic_key`, `labels`, `sprint`, `assignee`, `issue_type`), связи — `issue_link (source_key, target_key, link_type)` с семантикой Jira, заметки — `issue_note`, журнал синхронизаций — `sync_run`. HTTP — `http.server`, маршруты `ROUTES` с областью токена, ответы только JSON (`_send`). `GET /issues` постраничный и без связей. ## Goals / Non-Goals **Goals:** доска на живых данных хаба на Mac mini без ручной выгрузки; обновление из Jira кнопкой; перенос раскладки старой страницы; API, пригодный будущему React-приложению. **Non-Goals:** запись из доски (`viewer-edit`), React-приложение, доступ из сети, изменение логики доски. ## Decisions 1. **Одна страница — статика хаба.** Файл `src/jira_hub/web/links.html` входит в пакет (hatch включает файлы каталога пакета), читается при запросе через `Path(__file__).parent / "web" / "links.html"`. Маршруты `GET /` и `GET /links` без области токена (в `ROUTES` — область `None`, `_auth` не вызывается); ответ — `text/html; charset=utf-8`, `Cache-Control: no-cache` (страница меняется вместе с хабом). В `api` — отправка не-JSON ответа (обработчик возвращает `Raw(content_type, bytes)`). 2. **`GET /board` — сырой снимок, а не модель доски.** Перевод в модель (вид работы по меткам `kind-*`, имена эпиков, логические типы связей) остаётся на странице — там он уже есть для CSV, и React-приложению нужен тот же нейтральный формат. Чтение — в одной транзакции (`BEGIN` на чтение: задачи, связи и `last_sync` из одного момента). Функция `mirror.board(conn) -> dict`; `api` только оборачивает. 3. **`last_sync` — последний завершённый полный прогон:** `sync_run` с `kind = 'jql'`, `query` = текущая `base_jql`, `finished_at` не пуст, по `started_at` последний. Обновления по ключам (хвостовые `refresh` пакетов) в «данные на» не считаются: они обновляют часть задач. 4. **Вход по токену в браузере.** Токен — в `localStorage["jira-hub-token"]`, заголовок `Authorization: Bearer` в каждом `fetch` к тому же хосту; 401 → форма токена с пояснением. Токен не попадает в HTML, URL и логи хаба. 5. **«Обновить из Jira» через существующие маршруты.** `GET /agent` (офлайн → предупреждение, пакет всё равно ставится), `POST /refresh {full: true, author: "viewer"}`, опрос `GET /batches/{id}` каждые 3 с до терминального статуса пакета, затем `GET /board`. Кнопка недоступна, пока пакет идёт; перезагрузка страницы во время пакета не теряет его — пакет выполнится, «Перечитать» покажет результат. 6. **Модель доски из снимка (`fromHub`).** Эпики (`issue_type` «Фича»/Epic) — имена дорожек (`epic_key` → `summary`), сами карточками не становятся; остальные задачи, включая соседей, — карточки; вид работы — первая метка из `KIND_LABELS`, иначе `issue_type`; срок — `planned_end`, иначе `duedate`; связи — только типы из `LINK_TYPES`, `src = source`, `dst = target`; ключи из связей, которых нет в зеркале, — заглушки на дорожке «Нет в зеркале». Имя источника — «Хаб». 7. **Импорт JSON в режиме хаба** сохраняет раскладку, дорожки и фильтры (как сейчас) и вместо показа снимка из JSON перечитывает `GET /board`. Загрузка CSV — снимок файла с пометкой «из файла»; «Перечитать» возвращает данные хаба. 8. **Встроенная выгрузка удаляется** (`DEFAULT_CSV`, `DEFAULT_NAME` и их использование в `migrateStore`): без хаба страница показывает причину и предлагает загрузить файл. Константа `EPICS` остаётся запасным словарём имён для CSV. 9. **Проверка страницы — сценарий в браузере.** Тестовой обвязки для JS в хабе нет; страница проверяется прогоном через Playwright на хабе с тестовой базой (`JIRA_HUB_HOME` во временном каталоге, зеркало заполнено через `POST /agent/state`) и затем на живом хабе: вход, доска, «Обновить из Jira», «Перечитать», импорт JSON, неверный токен, остановленный хаб. Сторона хаба — pytest. ## Risks / Trade-offs - [Снимок всего зеркала на каждый запрос] → ~200 задач и сотни связей — десятки КБ; при росте — фильтр по спринту в параметрах (не сейчас). - [Токен в `localStorage` доступен скриптам страницы] → страница своя, сторонних скриптов нет; доступ только с Mac mini. - [Расхождение `KIND_LABELS`/`LINK_TYPES` страницы с настройками хаба (`kind_labels`, `link_types`)] → словари совпадают на 14.10.2026; синхронизация из `/settings` — в React-приложении. - [Страница отдаётся без токена] → в ней нет данных; все данные — через API с токеном. ## Migration Plan 1. Merge → `git pull` в `jira-hub` → `launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`. 2. Открыть `http://127.0.0.1:8790/`, ввести клиентский токен (`uv run jira-hub tokens`). 3. В старой странице «Спринт 2 линк» — «экспорт JSON», в новой — загрузить этот JSON: раскладка применится к живым данным. 4. Откат: предыдущий `main` и перезапуск; старая страница в `admin/` работает как раньше. ## Open Questions Нет. ## Decisions made without discussion | Маркер | Дата | Решение | Обоснование | Цена ошибки | |---|---|---|---|---| | R-1 (design #2) | 2026-10-14 | `GET /board` отдаёт сырые данные зеркала, перевод в модель доски — на странице | нейтральный формат нужен React-приложению; разбор уже есть на странице | лишний перевод на клиенте — дёшево переделать | | R-2 (design #3) | 2026-10-14 | «Данные на» — время последнего полного прогона, обновления по ключам не учитываются | частичное обновление не делает всю доску свежей | владелец может видеть время старше последних изменений по отдельным задачам | | R-3 (design #5) | 2026-10-14 | Пакет обновления — автор `viewer`, опрос каждые 3 с | автор отличает пакеты доски от `claude`/`hub`; 3 с — ощутимо и не грузит хаб | нет | | R-4 (design #6) | 2026-10-14 | Соседи по связям — обычные карточки, заглушки только для ключей вне зеркала | у соседей в зеркале полные данные | соседей без спринта скрывает фильтр спринта, как и раньше | | R-5 (design #8) | 2026-10-14 | Встроенная выгрузка от 11.10.2026 удаляется | устаревшие данные вводят в заблуждение; файл худеет на ~19 КБ | без хаба доска пуста до загрузки файла | | R-6 (design #3) | 2026-10-14 | `last_sync` учитывает только полный прогон, команда которого `done`; `failed` и отменённый прогон (у них тоже есть `finished_at`) не считаются | неудачный прогон не обновил данные — «данные на» не должно сдвигаться | нет: неудачный прогон виден в журнале синхронизаций | | R-7 (design #5) | 2026-10-14 | После перезагрузки страница находит незавершённый пакет с автором `viewer` (`GET /batches?limit=20`) и продолжает показывать его ход; кнопка «Обновить из Jira» недоступна | без этого перезагрузка позволяет поставить второй полный прогон подряд | лишний запрос при открытии страницы | | R-8 (design #6) | 2026-10-14 | Связи, у которых один из концов — эпик, на доске не показываются | эпик — дорожка, а не карточка; иначе он появился бы заглушкой «нет в зеркале», хотя в зеркале есть | связь задачи с эпиком не видна на доске (видна в Jira) | | R-9 (design #7) | 2026-10-14 | Импорт JSON при недоступном хабе показывает снимок из файла с пометкой «из файла»; при 401 — форма входа | спека требует запасной путь без хаба; снимок из JSON — то, что у пользователя есть | нет | | R-10 (design #1) | 2026-10-14 | В страницу добавлены `` и `` | старая страница писалась под обёртку артефакта, которая добавляет doctype; без него браузер включает режим совместимости | возможны мелкие отличия вёрстки от артефакта — проверяется скриншотами | | R-11 (design #4) | 2026-10-14 | Пункт меню «Сменить токен хаба»; токен перед сохранением проверяется на печатные ASCII-символы | после пересоздания токенов нужен выход без очистки хранилища вручную; `fetch` с не-ASCII заголовком падает как сетевая ошибка и дал бы ложное «хаб не отвечает» | нет | | R-12 (design #7) | 2026-10-14 | Пометка «(из файла)» и для CSV, и для JSON (вместо «(из JSON)»); ссылка загрузки «заменить…» переименована в «файл…» | спека говорит «из файла»; в режиме хаба загрузка файла не заменяет источник | нет | | R-13 (design #5) | 2026-10-14 | Неудачное «Перечитать» при показанной доске оставляет доску и пишет предупреждение; пустая доска с причиной — только если показывать нечего | стирать рабочую доску из-за перезапуска хаба неудобно | пользователь может не заметить, что данные не обновились — предупреждение в шапке | | R-14 (design #4) | 2026-10-14 | Текст при 401: «Токен не подошёл. Проверьте его по uv run jira-hub tokens: токен могли пересоздать.» | для только что введённого неверного токена прежнее «возможно, его пересоздали» сбивало | нет | | R-15 (design #1) | 2026-10-14 | `` и `.bar>span:empty{display:none}` | хаб отвечал 404 на `/favicon.ico` (ошибка в консоли на каждом открытии); пустые списки спринта/исполнителя давали отступ в строке фильтров | нет | | R-16 (design #8) | 2026-10-14 | Пустая доска без хаба: «Нет данных: <причина>. Запустите хаб (jira-hub serve) и нажмите «Перечитать» или загрузите файл CSV…» | прежний шаблон давал «Хаб не отвечает (хаб не отвечает)» | нет | | R-17 (design #7) | 2026-10-14 | Импорт JSON при недоступном хабе пишет в шапке «хаб не отвечает — показан снимок из <файл>» | текст `loadHub` «на доске прежние данные» неверен после замены доски снимком | нет | | R-18 (design #5) | 2026-10-14 | Строка состояния хаба переносится (`#hubState{white-space:normal}`) | предупреждение «агент офлайн — …; пакет #N…» без переноса вылезало бы за край на узком окне | нет | | R-19 (design #7) | 2026-10-14 | Подсказка окна экспорта JSON ссылается на «файл…» вместо «заменить…» | следствие R-12 | нет | | R-20 (design #6) | 2026-10-14 | Связи из зеркала берутся только между ключами вида `AURORA-1234` (`KEY_RE`), как в разборе CSV/JSON (финальное ревью, I-1) | ключ связи попадает в разметку карточки; строка зеркала с разметкой в `target_key` исполнялась на странице с токеном | связь с ключом нестандартного вида не видна на доске |