Andrey Epifancev 26d43acac3 docs(openspec): viewer-live — решения исполнения R-14…R-20 (#17)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:21:55 +04:00

99 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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