16 KiB
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
- Одна страница — статика хаба. Файл
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)). GET /board— сырой снимок, а не модель доски. Перевод в модель (вид работы по меткамkind-*, имена эпиков, логические типы связей) остаётся на странице — там он уже есть для CSV, и React-приложению нужен тот же нейтральный формат. Чтение — в одной транзакции (BEGINна чтение: задачи, связи иlast_syncиз одного момента). Функцияmirror.board(conn) -> dict;apiтолько оборачивает.last_sync— последний завершённый полный прогон:sync_runсkind = 'jql',query= текущаяbase_jql,finished_atне пуст, поstarted_atпоследний. Обновления по ключам (хвостовыеrefreshпакетов) в «данные на» не считаются: они обновляют часть задач.- Вход по токену в браузере. Токен — в
localStorage["jira-hub-token"], заголовокAuthorization: Bearerв каждомfetchк тому же хосту; 401 → форма токена с пояснением. Токен не попадает в HTML, URL и логи хаба. - «Обновить из Jira» через существующие маршруты.
GET /agent(офлайн → предупреждение, пакет всё равно ставится),POST /refresh {full: true, author: "viewer"}, опросGET /batches/{id}каждые 3 с до терминального статуса пакета, затемGET /board. Кнопка недоступна, пока пакет идёт; перезагрузка страницы во время пакета не теряет его — пакет выполнится, «Перечитать» покажет результат. - Модель доски из снимка (
fromHub). Эпики (issue_type«Фича»/Epic) — имена дорожек (epic_key→summary), сами карточками не становятся; остальные задачи, включая соседей, — карточки; вид работы — первая метка изKIND_LABELS, иначеissue_type; срок —planned_end, иначеduedate; связи — только типы изLINK_TYPES,src = source,dst = target; ключи из связей, которых нет в зеркале, — заглушки на дорожке «Нет в зеркале». Имя источника — «Хаб». - Импорт JSON в режиме хаба сохраняет раскладку, дорожки и фильтры (как сейчас) и вместо показа снимка из JSON
перечитывает
GET /board. Загрузка CSV — снимок файла с пометкой «из файла»; «Перечитать» возвращает данные хаба. - Встроенная выгрузка удаляется (
DEFAULT_CSV,DEFAULT_NAMEи их использование вmigrateStore): без хаба страница показывает причину и предлагает загрузить файл. КонстантаEPICSостаётся запасным словарём имён для CSV. - Проверка страницы — сценарий в браузере. Тестовой обвязки для 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
- Merge →
git pullвjira-hub→launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub. - Открыть
http://127.0.0.1:8790/, ввести клиентский токен (uv run jira-hub tokens). - В старой странице «Спринт 2 линк» — «экспорт JSON», в новой — загрузить этот JSON: раскладка применится к живым данным.
- Откат: предыдущий
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 html> и <html lang="ru"> |
старая страница писалась под обёртку артефакта, которая добавляет 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 | <link rel="icon" href="data:,"> и .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 исполнялась на странице с токеном |
связь с ключом нестандартного вида не видна на доске |