Andrey Epifancev e95d0e0ce5 docs(openspec): архив viewer-live (#17)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:50:45 +04:00

16 KiB
Raw Blame History

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 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 исполнялась на странице с токеном связь с ключом нестандартного вида не видна на доске