2026-10-24 15:30:12 +04:00

147 lines
26 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
Основа — `main` после слияния sdlc-rename (#97): пакет `sdlc_hub`, бренд «SDLC Hub», ключи `sdlc-hub-*`. Ветка
`feat/98-web-modules` перебазируется на него до исполнения плана.
Аудит веба 24.10.2026 (сабагент, код + живой хаб, скриншоты в scratchpad сессии) — главное:
- контракт `HubModule` (`web/src/modules/types.ts`): `name`, `pages`, `nav`, `Provider`, `SidebarStatus`
(компонент), `TopbarNotice`, `useOverlay`, `projectFields`, `useOnProjectCreated(batchId)`;
- `RefreshProvider` (модуль jira, 174 строки) — единственный трекер пакетов: подхват пакетов `viewer` с первой
командой `refresh` (по `GET /batches/{id}` на каждого кандидата), один опрос, остановка, перечитывание; агент — `GET
/agent` (только jira); время — `last_sync` из снимка `/board` (грузится на каждой странице);
- wiki: `SyncStatus` — текст из `GET /projects` + `GET /wiki/status`, кнопка только в шапке страницы;
- ядро знает jira: `/` → `/board` в модуле jira, типы jira и `fact_status` в `api/types.ts`, `api/closed.ts`, бренд;
модуль wiki импортирует `fmtStamp` из модуля jira;
- `modules/jira/issues/wiki/wiki.css` (`div.wiki h1…`) перебивает заголовки страницы «Вики» (корень `div.wiki`);
- `POST /projects` → один `batch_id`, оболочка раздаёт его всем модулям;
- API: `GET /agents` уже есть (все поля агента), `GET /projects` уже несёт свежесть wiki, у пакета нет модуля.
## Goals / Non-Goals
**Goals:** оболочка рисует состояние модулей по их описанию; пакеты и агенты отслеживает ядро; навигация и главная
страница — модульные; граница ядра и модулей в вебе проверяется тестами; найденные аудитом дефекты страниц исправлены.
**Non-Goals и Anti-Goals:** см. proposal.md.
## Decisions
1. **Навигация — группы по модулям + «Обзор»** (вариант «А + Б» аудита). Группы оставляют все разделы на виду (их
четыре), «Обзор» даёт модульную главную вместо `/board` и место для сводки. Переключатель модулей (вариант «В»)
отклонён: лишний щелчок между доской и вики ради масштаба, которого нет.
2. **Адреса с префиксом модуля** (`/app/jira/…`, `/app/wiki`) и перенаправления со старых. Делается сейчас, пока
ломается всё сразу (переименование); пути модулей не пересекаются по построению. Перенаправления объявляет модуль
jira (это его прежние адреса). `GET /` хаба — на `/app/`.
3. **Состояние модуля — данные (`ModuleStatus`), рисует ядро.** Компонент `SidebarStatus` убирается: из-за него блок и
был разнобойным. Свежесть берётся из уже загруженного `GET /projects` чистой функцией от проекта — без запросов
модуля: jira — `jira_synced_at` (новое поле, общее для проектов), wiki — `wiki_synced_at`/`wiki_sync_error`.
4. **Один трекер пакетов в ядре** (`shell/activity/`): обобщение `RefreshProvider` — состояние на модуль
(`event`, `active`, `busy`), опрос `GET /batches/{id}`, правила остановки прежние. Подхват — по полю `module` из
`GET /batches?module=` (новое), любого автора: автообновления хаба и пакеты из MCP теперь видны (С1/В1 аудита), а
подхват не делает запрос на каждого кандидата. Прежнее правило «только viewer + refresh» отменено (сценарий «Пакет с
другими командами» меняется: пакет показывается, но не блокирует кнопку).
5. **Кнопка недоступна только на время пакета своего действия обновления** (jira — первая команда `refresh`, wiki —
`wiki_sync`; тип виден в `GET /batches/{id}`, который трекер и так опрашивает). Иначе долгое скачивание вложения
блокировало бы «Обновить из Jira».
6. **Обновление: значок в строке модуля + вторичная кнопка в шапке страниц модуля**, primary для обновления нет. Сейчас
на «Вики» две зелёные кнопки (в панели и в шапке) — против правила AURA «одна primary на экран».
7. **Агенты — один опрос `GET /agents`** раз в 60 с; точка агента: на связи / офлайн / «обновите агента» (`warning`
или `update_hint`), всегда с текстом.
8. **Узкий экран**: в верхней строке «Меню», метка проекта и сводная точка; отдельная кнопка «Обновить из Jira» и
`TopbarNotice` из контракта убираются (событие пакета показывает ядро строкой под верхней строкой). Выезжающая
панель — модальная (#73 M-1…M-3).
9. **Пакеты нового проекта по модулям**: `POST`/`PATCH /projects` отдают `batches: [{module, id}]`; `batch_id` и
`batch_ids` остаются для MCP. Оболочка передаёт каждому модулю его пакет (снимает M-14/T5.3 #68 и потерю `wiki_sync`).
10. **Граница в вебе — тестами**: импорты (ядро → только `modules/index.ts`; модуль → не другой модуль), уникальность
имён модулей и `nav.to` (T7.5/T7.6 #90), пересечение имён классов CSS между модулями и с ядром. Префиксы классов
не вводятся: тест на пересечение ловит именно случившийся дефект и не требует переименовывать сотни классов.
`div.wiki` разметки Jira переименовывается в `.jira-markup`.
11. **Типы jira — в модуль jira** (`modules/jira/api/types.ts`), `closed.ts` — туда же; `fact_status` удаляется из веба
(веб-часть #60). В `api/types.ts` ядра остаются `Batch`, `AgentView` (полный вид из `GET /agents`), `Project`,
`CreatedProject`, `UpdatedProject`, `HubSettings` без словарей jira.
12. **Форматтеры дат — ядро** (`ui/format.ts`): `fmtStamp` («ДД.ММ ЧЧ:ММ»), `fmtDate` («ДД.ММ», не текущий год —
«ДД.ММ.ГГГГ»). Списки и карточка перестают выводить ISO.
13. **«Обзор» берёт сводку у модуля** (`Summary({project})`); счётчики jira — из снимка доски проекта (запрос только на
«Обзоре»); вики — `wiki_pages` из проекта.
14. **Порядок архива**: sdlc-rename архивируется раньше web-modules: дельты `web-app`/`hub-api` этого change написаны
поверх текстов после переименования.
## Контракт веб-модуля (после change)
```ts
type ModuleStatus = {
agent?: string; // имя модуля агента в GET /agents
scope: "all" | "project"; // охват свежести и обновления
freshness(p: Project): { available: boolean; at: string | null; error: string | null };
refresh?: {
label: string; // «Обновить из Jira», «Обновить вики»
start(project: string): Promise<{ id: number; estimate_seconds?: number }>; // 409 → ApiError, ядро подхватит идущий
isOwn(b: Batch): boolean; // пакет этого действия (по commands[0].type)
};
invalidate(qc: QueryClient, project: string | null): void; // что перечитать после пакета модуля
};
type HubModule = {
name: string; title: string; // «jira» / «Jira»
pages: RouteObject[]; nav: NavItem[]; // nav.to — с префиксом модуля
status?: ModuleStatus;
Summary?: ComponentType<{ project: Project }>; // карточка «Обзора»
Provider?, useOverlay?, projectFields?; // как было; SidebarStatus, TopbarNotice, useOnProjectCreated — удалены
};
```
## Risks / Trade-offs
- **Подхват пакетов любого автора** покажет и пакеты, поставленные из чата (`claude`), — это желаемо, но строка «Jira»
будет чаще «занята». Кнопка при этом не блокируется (решение 5).
- **Счётчики на «Обзоре»** грузят снимок доски — тяжёлый запрос; только на этой странице и с тем же ключом кэша, что у
доски.
- **Смена адресов** ломает закладки — их держат перенаправления; внешних ссылок на веб нет (MCP веб-адресов не отдаёт).
## Decisions made without discussion
Владелец 24.10.2026 ответил «делай» на вопросы brainstorming — решения 1–14 выше приняты агентом по аудиту.
| # | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 24.10.2026 | Навигация — группы + «Обзор» | 4 раздела, 2 модуля; нужна модульная главная | переделка панели |
| 2 | 24.10.2026 | Адреса `/app/<модуль>/…` с перенаправлениями | заодно с переименованием, пути не пересекаются | вернуть пути, перенаправления наоборот |
| 3 | 24.10.2026 | Состояние модуля — данные, рисует ядро | разнобой блоков возник из компонента | — |
| 4 | 24.10.2026 | Трекер в ядре, подхват любого автора по `module` | видны автообновления и пакеты MCP | вернуть фильтр по автору |
| 5 | 24.10.2026 | Блокирует только пакет своего обновления | долгие пакеты не мешают обновлению | — |
| 6 | 24.10.2026 | Обновление — значок в панели + secondary в шапке | одна primary на экран | перекраска кнопок |
| 7 | 24.10.2026 | Один опрос `/agents` | агент wiki не виден сейчас | — |
| 8 | 24.10.2026 | Узкий экран: метка проекта + сводная точка | проект не виден, две кнопки обновления | вернуть кнопку |
| 9 | 24.10.2026 | `batches` по модулям, старые поля остаются | пакет wiki терялся; MCP не ломать | — |
| 10 | 24.10.2026 | Тест на пересечение классов CSS вместо префиксов | ловит дефект без массового переименования | ввести префиксы позже |
| 11 | 24.10.2026 | Типы jira — в модуль, `fact_status` из веба удалён | граница ядра; #60 | вернуть поле в тип |
| 12 | 24.10.2026 | Форматтеры дат в ядре | три формата дат, импорт wiki → jira | — |
| 13 | 24.10.2026 | Сводка «Обзора» от модуля | ядро не знает данных модулей | — |
| 14 | 24.10.2026 | Архив после sdlc-rename | дельты поверх новых имён | правка дельт при архиве |
| 15 | 24.10.2026 | `last_full_sync(conn, base_jql=None)` в `modules/jira/mirror.py`: базовая JQL необязательна (по умолчанию `projects.base_jql`), `board()` передаёт свою; импорт в `project_fields.view` отложенный | интерфейс плана `(conn)` сохранён, `board()` не меняет сигнатуру; прямой импорт даёт цикл mirror → history → projects → project_fields | вернуть явный параметр |
| 16 | 24.10.2026 | Фильтр `module` в `list_batches` применяется до `limit` перебором пакетов (по типу первой команды), без SQL-колонки | у пакета нет колонки модуля, схему не менять; объём пакетов невелик | добавить колонку `batch.module` |
| 17 | 24.10.2026 | Тесты Task 1 лежат в `test_wiki_projects.py`/`test_project_fields.py` (нужны оба модуля), а не в `test_queue_core.py`/`test_api.py`; правлены старые ожидания в `test_db.py` (`jira_`-поля) | в `test_queue_core` модули подменены заглушкой | перенести тесты |
| 18 | 24.10.2026 | R-1: трейлер коммитов ветки — `Co-Authored-By: Claude Sonnet 5.5`, а не `Opus 5.5` из плана | системная подсказка сессии об атрибуции приоритетнее текста плана | переписать трейлеры |
| 19 | 24.10.2026 | R-2: предусловие плана («после слияния #97 в main») считается выполненным базированием на `feat/97-sdlc-rename` | указание владельца; PR #99 ещё не слит | ребейз на main после слияния #99 |
| 20 | 24.10.2026 | R-3: параметр `q` страницы «Вики» реализует Task 5, Task 7 только добавляет тест | в плане требование задвоено между Task 5 и Task 7 | мелкая правка |
| 21 | 24.10.2026 | Task 2: `ModuleProviders.tsx` перенесён из `modules/` в `shell/` (импортирует только `modules/index.ts`) | тест-страж границы допускает из ядра лишь `modules/index.ts` и `modules/types.ts`; `App.tsx` импортировал `modules/ModuleProviders.tsx` | вернуть файл и ослабить страж |
| 22 | 24.10.2026 | Task 2: `fmtDate` доски (`board/model/util.ts`, принимает не-ISO даты Jira, год не показывает) остаётся в модуле; в ядре `ui/format.ts` — свой `fmtDate`, таблица и карточка задач берут его (даты другого года — `ДД.ММ.ГГГГ`); `fmtStamp(null)` → «—» | задача требует форматтеры в ядре; доска сравнивает строки дат Jira в разных локалях | свести две функции в одну |
| 23 | 24.10.2026 | Task 2: словари jira-настроек — тип `JiraSettings extends HubSettings` в `modules/jira/api/types.ts`; `HubSettings` ядра — только `project_key` и индексная подпись | страж запрещает термины jira в ядре | вернуть словари в `HubSettings` |
| 24 | 24.10.2026 | Task 3: `ActivityProvider` стоит в `Layout` (внутри `ProjectContext`), а не в `App.tsx`; «проект создан» — `trackBatch(batchId)` ядра (модуль берётся из `Batch.module`, запасной путь — `refresh.isOwn`) вместо `useOnProjectCreated` модуля | `project` провайдера берётся из `ProjectContext`, он создаётся в `Layout`; модуль не называется в ядре | перенести провайдер выше и передавать проект иначе |
| 25 | 24.10.2026 | Task 3: подхват пакетов — только при монтировании, смене проекта, раз в 60 с и после 409; прежний подхват «после загрузки снимка доски» (`skipPickup`, T10.1/T4.1) убран | ядро не знает ключей данных модуля; правила подхвата заданы спекой; счётчик `skipPickup` больше не нужен | вернуть подписку на кэш по ключу, объявляемому модулем |
| 26 | 24.10.2026 | Task 3: состояние на модуль — хук-трекер на модуль (`useTracker`, вызывается по константному списку `TRACKED`) и `useQuery` пакета в каждом, а не общий `useQueries` | перенос логики остановки опроса без изменений; `useQueries` потребовал бы переписать эффект завершения | заменить на `useQueries` |
| 27 | 24.10.2026 | Task 3: `start` берёт агента свежим `fetchQuery(["agents"], staleTime 0)`, а не из кэша; ошибка этого запроса — событие `hub-down`/`error`, как у прежнего `GET /agent` | сохраняет поведение прежних сценариев (hub-down, 401 на предпроверке) | брать из кэша `useAgents()` |
| 28 | 24.10.2026 | Task 3: wiki `refresh.start` приводит ответ `{batch_id}` к `{id}`; `track()` перехватывает отслеживание, если текущий пакет не «свой» (раньше — только если ничего не отслеживалось) | контракт ядра единый `{id}`; пакет нового проекта важнее чужого | отдельная форма ответа / прежняя проверка |
| 29 | 24.10.2026 | Task 3: сводка `useActivitySummary`: error > offline > busy > online | ошибка и офлайн агента важнее, чем «идёт пакет» | поменять порядок |
| 30 | 24.10.2026 | Task 3: временный `shell/activity/StatusBlock.tsx` (строки модулей и строка событий узкого экрана) вместо `SidebarStatus`/`TopbarNotice` модулей; Task 4 заменяет его; страница вики берёт действие из трекера, уведомление об ошибке постановки (`syncError`) убрано — ошибка видна событием модуля | оболочка должна работать и набор быть зелёным до Task 4 | вернуть Notice на странице вики |
| 31 | 24.10.2026 | Task 3, fix T3.I1: подхват после 409 (`pickup(true)`) заменяет отслеживаемый чужой пакет первым (самым старым) другим незавершённым пакетом модуля; если другого нет — чужой остаётся | детали 409 (`batch_id`) клиент не разбирает, а список без команд не различает типы | брать `batch_id` из `ApiError.detail` |
| 32 | 24.10.2026 | Task 4: «Обзор» (`/`) пока рисует `shell/OverviewPage.tsx` — заглушка с заголовком под `ProjectGate`; Task 5 заменяет её страницей со сводками | пункт «Обзор» и адрес `/` нужны панели и перенаправлениям уже сейчас | оставить `/` → `/jira/board` до Task 5 |
| 33 | 24.10.2026 | Task 4: пункты панели ядра — `shell/nav.tsx` (`OVERVIEW`, `GROUPS`, `NAV`); NotFound, Layout и Sidebar берут их оттуда; группа — `role="group"` с видимым названием модуля (в свёрнутой панели название скрыто) | один источник списка разделов для панели, «не найдено» и заголовка вкладки | свести в `MODULES.flatMap` по месту |
| 34 | 24.10.2026 | Task 4: список под окном карточки по прямому/прежнему адресу берёт `project` из адреса карточки (`listBackground(search)`), закрытие — `/jira/issues?project=…` | брифом: «закрытие окна — с текущим проектом»; константа `LIST_BACKGROUND` без проекта теряла выбор | вернуть константу, проект подставит Layout |
| 35 | 24.10.2026 | Task 4: модальность выезжающей панели — атрибут `inert` на соседях `aside` внутри `.shell` (страница и верхняя строка), фокус на «Меню» возвращается эффектом после закрытия; `Dropdown` гасит Escape (`preventDefault`) только когда сам открыт, а панель закрывается по Escape лишь при `!defaultPrevented` | ловушка Tab на списке фокусируемых хрупка; `inert` закрывает и чтение с экрана | ловушка фокуса вручную |
| 36 | 24.10.2026 | Task 4: `--topbar-h` удалён из `shell.css` и из `board.css` (`.drawer` на ширине страницы < 852 px статичен, а строка приложения бывает только на < 640 px) | M-6 #73: переменная мертва | вернуть переменную |
| 37 | 24.10.2026 | Task 4: прокручивается середина панели (`.sidebar__scroll`: разделы и блок состояния), низ с темой и меню «Ещё» остаётся вне прокрутки; поправка R-1 решения #69 (меню «Ещё» свёрнутой панели выходит за 64 px, при `overflow` на всей панели обрезалось бы) | M-5 #73: на низком окне панель не должна обрезаться | прокрутка всей панели и обрезанное меню |
| 38 | 24.10.2026 | Task 4: сводная точка узкого экрана — `role="img"` с `aria-label` «Состояние модулей: ошибка / идёт обновление / агент на связи / агент офлайн»; «идёт обновление» — нейтральный цвет (токена «info» нет); событие пакета узкого экрана — постоянный `role="status"` под строкой (`data-testid="topbar-events"`) | цвет не единственный признак; T10.1 #94 | вернуть `aria-hidden`-строку |
| 39 | 24.10.2026 | Task 5: `GET /board` на `/` («Обзор» грузит снимок доски для сводки jira, решение 13) — тест `no wiki row without root` блока состояния проверяет только отсутствие `GET /wiki/status` | `open()` теста открывает `/`, а он теперь тянет доску; суть теста — строка вики без корня | тест строже, чем нужно; ослабление скрывает запрос статуса вики без корня |
| 40 | 24.10.2026 | Task 5: общий `agentText(a)` (`shell/activity/agentText.ts`) — текст, подсказка и признак «на связи» агента; им пользуются блок состояния и карточки «Обзора» | текст «агент на связи/офлайн/обновите агента» не должен разойтись между двумя местами | рефакторинг блока состояния; тесты блока его держат |
| 41 | 24.10.2026 | Task 5: `useRecentBatches(project)` опрашивает `GET /batches?project=&limit=10` раз в 15 с (`refetchInterval`) | ход пакетов меняется без участия страницы; инвалидация из активности ядра усложнила бы контракт | лишние запросы раз в 15 с на «Обзоре» |
| 42 | 24.10.2026 | Task 5: поле поиска вики на «Обзоре» не ведёт на пустой запрос (Enter при пустой строке ничего не делает); `q` в «Вики» — только начальное значение поля, выбор страницы убирает `q` из адреса | пустой поиск = страница «Вики» без `q`, для неё есть ссылка «Вики»; `hrefOf` страницы по контракту Task 4 не несёт поиск | пользователь, вставший на `?q=` и открывший страницу, теряет в адресе строку поиска (в поле она остаётся) |