docs(openspec): change viewer-live — доска связей на живых данных хаба (#17)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fc732ebf91
commit
a6a9e5d4d6
@ -1,7 +1,7 @@
|
||||
# jira-hub
|
||||
|
||||
## Что это
|
||||
Хаб моста Jira для АРМ КА: единственный источник истины по задачам проекта AURORA (метка AWPCA) на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; текущего change нет (последний — `openspec/changes/archive/2026-10-14-bridge-hygiene/`).
|
||||
Хаб моста Jira для АРМ КА: единственный источник истины по задачам проекта AURORA (метка AWPCA) на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; текущий change — `openspec/changes/viewer-live/`.
|
||||
|
||||
## Стек и ограничения
|
||||
- Python 3.13, uv, hatchling, pytest; HTTP на stdlib (`http.server`), БД — SQLite (stdlib), MCP — пакет `mcp` (закреплён `<2`, рулинг R-10).
|
||||
|
||||
2
openspec/changes/viewer-live/.openspec.yaml
Normal file
2
openspec/changes/viewer-live/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-14
|
||||
83
openspec/changes/viewer-live/design.md
Normal file
83
openspec/changes/viewer-live/design.md
Normal file
@ -0,0 +1,83 @@
|
||||
## Context
|
||||
|
||||
Просмотрщик «Спринт 2 линк» — один HTML-файл (~117 КБ, из них ~100 КБ — встроенная выгрузка 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 удаляется | устаревшие данные вводят в заблуждение; файл худеет со ~117 до ~17 КБ | без хаба доска пуста до загрузки файла |
|
||||
41
openspec/changes/viewer-live/proposal.md
Normal file
41
openspec/changes/viewer-live/proposal.md
Normal file
@ -0,0 +1,41 @@
|
||||
## Why
|
||||
|
||||
Просмотрщик «Спринт 2 линк» (`admin/sprint-2-links.html`, артефакт на claude.ai) — граф блокировок спринта с раскладкой
|
||||
по эпикам и датам, критическим путём и панелью проблем — работает на ручной выгрузке CSV из фильтра Jira. С 13.10.2026
|
||||
в работе мост jira-hub/jira-agent: зеркало задач, связей и заметок обновляется из Jira командой `refresh` через очередь.
|
||||
Ручная выгрузка CSV больше не нужна: доска может читать зеркало хаба напрямую.
|
||||
|
||||
Решения владельца 14.10.2026 (brainstorming):
|
||||
- вариант A: сначала живые данные на текущей одностраничной версии, затем отдельным change — веб-приложение на React
|
||||
(React + TypeScript + Vite, статика отдаётся хабом), где доска станет одной из страниц; API проектируется под него;
|
||||
- страницу открывают пока только на Mac mini; вход — клиентский токен хаба, вводится один раз и хранится в браузере
|
||||
(вариант 1; без исключений для 127.0.0.1 и без вшивания токена в страницу);
|
||||
- раскладка из старой страницы переносится существующими экспортом/импортом JSON.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **jira-hub**:
|
||||
- `GET /board` (клиентский токен) — согласованный снимок зеркала: задачи (включая эпики и соседей) с заметками
|
||||
`fact_status`/`work_kind`, связи, время последнего полного прогона; данные без преобразований;
|
||||
- `GET /` и `GET /links` — страница доски связей без токена (статический файл из пакета `src/jira_hub/web/links.html`);
|
||||
- страница — развитие `admin/sprint-2-links.html`: вход по токену, загрузка из `GET /board`, «Обновить из Jira»
|
||||
(пакет `refresh` с автором `viewer` и ожидание его), «Перечитать», импорт JSON в режиме хаба применяет только
|
||||
раскладку и фильтры; встроенная выгрузка CSV от 11.10.2026 удаляется; CSV-файл — запасной путь.
|
||||
- Спека `hub-api` — MODIFIED «Процесс и доступ» (исключение для статической страницы), ADDED «Снимок доски» и «Страница
|
||||
доски связей».
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Запись из страницы (перенос в спринт, срок, связи, исполнитель командами очереди) — change `viewer-edit`.
|
||||
- Веб-приложение на React — следующий change; эта страница в нём будет перенесена, не расширена.
|
||||
- Доступ со других машин сети, вход без токена, хранение раскладки в базе хаба.
|
||||
- Изменение логики доски (граф, раскладка, критический путь, проблемы, таблица, тема).
|
||||
- Переопубликация артефакта на claude.ai (спека migration: не переопубликовывается).
|
||||
|
||||
## Impact
|
||||
|
||||
- `jira-hub`: `api.py` (маршруты `/board`, `/`, `/links`; отправка не-JSON ответа), новый модуль чтения снимка
|
||||
(или функция в `mirror`), `src/jira_hub/web/links.html`, тесты API, `ARCHITECTURE.md`, `CLAUDE.md`, README.
|
||||
- `admin/sprint-2-links.html` остаётся в `admin/` как история, не правится.
|
||||
- Контракт с агентом не меняется. Новых зависимостей нет.
|
||||
- Issue: aep/jira-hub#17.
|
||||
91
openspec/changes/viewer-live/specs/hub-api/spec.md
Normal file
91
openspec/changes/viewer-live/specs/hub-api/spec.md
Normal file
@ -0,0 +1,91 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Процесс и доступ
|
||||
`jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:<port>` (по умолчанию 8790), база и настройки в
|
||||
`~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты API SHALL требовать
|
||||
`Authorization: Bearer <token>`: агентский токен открывает только `/agent/*`, клиентский — всё остальное. Исключение —
|
||||
страница доски связей (`GET /`, `GET /links`): это статический файл без данных, он отдаётся без токена. Ответы API —
|
||||
JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или `Content-Length`), 401, 404, 409, 413 (тело больше
|
||||
предела), 422, 500 (внутренняя ошибка); реле общей папки дополнительно отвечает 502. Методы, кроме GET, POST и PUT,
|
||||
отвечают 501 стандартной страницей `http.server`, не JSON.
|
||||
|
||||
#### Scenario: Чужой токен
|
||||
- **WHEN** агентским токеном вызван `GET /issues`
|
||||
- **THEN** 401
|
||||
|
||||
#### Scenario: Неподдерживаемый метод
|
||||
- **WHEN** `DELETE /issues/AURORA-1`
|
||||
- **THEN** 501, тело не JSON; база не меняется
|
||||
|
||||
#### Scenario: Страница без токена
|
||||
- **WHEN** `GET /` без заголовка `Authorization`
|
||||
- **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — страница доски связей; данных зеркала в ней нет
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Снимок доски
|
||||
Хаб SHALL предоставлять `GET /board` под клиентским токеном — согласованный снимок зеркала для доски связей, прочитанный
|
||||
в одной транзакции:
|
||||
- `generated_at` — момент снимка (UTC);
|
||||
- `last_sync` — последний завершённый полный прогон (`refresh` по базовой JQL): `{started_at, finished_at, issues_count}`
|
||||
или `null`, если такого не было;
|
||||
- `issues` — все задачи зеркала, включая эпики и соседей: `key`, `summary`, `issue_type`, `status`, `priority`,
|
||||
`assignee`, `sprint`, `duedate`, `planned_start`, `planned_end`, `epic_key`, `labels` (список), `neighbor` (bool),
|
||||
`fact_status`, `work_kind` (из заметок, `null` если заметки нет); порядок — по ключу;
|
||||
- `links` — все связи зеркала: `{source, target, link_type}`, `link_type` — имя типа в Jira, направление как в
|
||||
`issue_link` (`source` блокирует/ссылается на `target`).
|
||||
Перевод в модель доски (вид работы по меткам, имена эпиков, логические типы связей) — дело интерфейса; хаб отдаёт данные
|
||||
зеркала без преобразований.
|
||||
|
||||
#### Scenario: Снимок с задачами и связью
|
||||
- **WHEN** в зеркале эпик AURORA-1351, задачи AURORA-1620 (метка `kind-dev`, эпик AURORA-1351) и AURORA-1621, связь
|
||||
`AURORA-1620 → AURORA-1621` типа `ASUPR_Блокер_Начала`, заметка `work_kind = "разработка"` у AURORA-1620
|
||||
- **THEN** `GET /board` отдаёт три задачи по ключу, у AURORA-1620 `labels = ["kind-dev"]`, `epic_key = "AURORA-1351"`,
|
||||
`work_kind = "разработка"`; в `links` — `{source: "AURORA-1620", target: "AURORA-1621", link_type: "ASUPR_Блокер_Начала"}`
|
||||
|
||||
#### Scenario: Последний полный прогон
|
||||
- **WHEN** в журнале синхронизаций полный прогон, завершённый в 10:00, и после него обновление по ключам в 11:00
|
||||
- **THEN** `last_sync.finished_at` — 10:00
|
||||
|
||||
#### Scenario: Без токена
|
||||
- **WHEN** `GET /board` без токена или с агентским токеном
|
||||
- **THEN** 401
|
||||
|
||||
### Requirement: Страница доски связей
|
||||
Хаб SHALL отдавать по `GET /` и `GET /links` страницу доски связей — один HTML-файл из пакета хаба
|
||||
(`src/jira_hub/web/links.html`, развитие просмотрщика «Спринт 2 линк»). Страница SHALL:
|
||||
- спрашивать клиентский токен хаба при первом открытии и после ответа 401, хранить его в `localStorage` браузера и
|
||||
передавать в заголовке `Authorization` своих запросов к API того же хоста;
|
||||
- при открытии загружать `GET /board` и строить доску из него: задачи-эпики — дорожки, остальные задачи (включая
|
||||
соседей) — карточки; вид работы — по меткам `kind-*`, иначе по типу задачи; срок карточки — `planned_end`, при его
|
||||
отсутствии `duedate`; связи — по известным странице типам Jira, неизвестные типы пропускаются;
|
||||
- показывать в шапке время последнего полного прогона (`last_sync.finished_at`);
|
||||
- по кнопке «Обновить из Jira» ставить `POST /refresh {full: true, author: "viewer"}`, показывать ход пакета по
|
||||
`GET /batches/{id}` до завершения и затем перечитывать `GET /board`; если агент офлайн (`GET /agent`), предупреждать,
|
||||
но пакет ставить; при ошибке пакета — показывать её текст;
|
||||
- по кнопке «Перечитать» загружать `GET /board` заново без обращения к Jira;
|
||||
- сохранять раскладку карточек, порядок и свёртку дорожек, фильтры в `localStorage` под ключами, не зависящими от
|
||||
источника данных; импорт JSON, выгруженного страницей, в режиме хаба SHALL сохранять эти настройки и сразу
|
||||
перечитывать данные хаба, а не показывать снимок из файла;
|
||||
- оставлять загрузку CSV из фильтра Jira как запасной путь (снимок с пометкой «из файла»; «Перечитать» возвращает данные
|
||||
хаба);
|
||||
- при недоступном хабе показывать понятное сообщение и предлагать загрузить файл; встроенной выгрузки данных страница
|
||||
не содержит.
|
||||
|
||||
#### Scenario: Первый вход
|
||||
- **WHEN** страница открыта впервые, токена в `localStorage` нет
|
||||
- **THEN** вместо доски — поле клиентского токена с подсказкой `uv run jira-hub tokens`; после ввода верного токена доска
|
||||
строится из `GET /board`, токен сохранён
|
||||
|
||||
#### Scenario: Неверный токен
|
||||
- **WHEN** сохранённый токен устарел и `GET /board` ответил 401
|
||||
- **THEN** страница снова показывает поле токена с пояснением, что токен не подошёл
|
||||
|
||||
#### Scenario: Обновление из Jira
|
||||
- **WHEN** нажата «Обновить из Jira», агент на связи
|
||||
- **THEN** в хабе пакет из одного `refresh` по базовой JQL с автором `viewer`; страница показывает «пакет #N выполняется»,
|
||||
по завершении перечитывает доску и показывает новое время данных
|
||||
|
||||
#### Scenario: Перенос раскладки
|
||||
- **WHEN** в режиме хаба импортирован JSON, выгруженный старой страницей «Спринт 2 линк»
|
||||
- **THEN** раскладка, дорожки и фильтры из JSON применены к задачам хаба; задачи и связи — из хаба, не из JSON
|
||||
Loading…
x
Reference in New Issue
Block a user