24 KiB
Context
25.10.2026 пять сабагентов сверили открытые follow-up issue хаба и агентов с main (hub 1405ccd). Каждый маркер получил
статус с доказательством file:line. Итоги записаны комментариями в issue. Владелец выбрал первым пакет A — видимые
пользователю ошибки веба (sdlc-hub#124). Brainstorming-вопросы владелец пропустил («делай сам… не хочу с ними
сталкиваться в будущем»), поэтому все решения ниже приняты агентом. Они перечислены в «Decisions made without
discussion».
Состав пакета: #42 M-1, M-2, M-4, M-5; #116; #103 С6 и метки kind-*; #56 M-4, M-5, M-6; #35 T4.1/M-15; #60 M-3;
#64 M-6, T9.1; #86 T6.2, T6.3, M-4; #68 T3.3. Ссылки на строки ниже — по main 1405ccd.
Goals / Non-Goals
Цель — убрать перечисленные ошибки. Каждую закрепить тестом, который падает до правки: юнит или поведение компонента в vitest, для окна карточки дополнительно e2e. Сервер, API, MCP и агенты не меняются.
Не цели:
- прочие маркеры исходных issue: тесты, стиль, доступность UI-кита (#68), квадратичный разбор (#56 M-2), фокус окна
(#42 M-3),
100vhокна (#42 M-6); - новые возможности.
Decisions
Окно карточки
-
Поиск списка не закрывает окно (#42 M-1). Сейчас
IssuesPage.tsx:31-38через 300 мс после ввода делаетsetParams(..., {replace: true}). Если за это время открыто окно карточки, запись заменяет адрес окна адресом списка, и окно закрывается.Решение — перед открытием карточки со страницы «Задачи» (строка, ключ) несохранённый поиск записывается в адрес сразу, таймер снимается. Окно открывается поверх списка уже с этим поиском. Отложенная запись MUST NOT срабатывать, пока открыто окно.
Реализация на усмотрение исполнителя, например:
IssuesPageдаёт таблице свою обёрткуopen, которая сначала делает flush;- или эффект проверяет, что текущий адрес браузера — сам список.
-
Повторное закрытие не уводит назад дальше (#42 M-2).
overlay.tsx:37-41:close()при каждом вызове делаетnavigate(-depth). Двойной щелчок по ✕ или фону, Esc сразу после щелчка — два шага назад.Решение — закрытие выполняется один раз для одной записи истории окна. Повторные вызовы игнорируются, пока адрес не сменился. Признак —
location.keyзаписи, с которой начато закрытие. Отметка снимается также при «Остаться» (abortClose): навигация заблокирована, ключ записи не меняется, и без сброса следующий ✕ молча игнорировался бы (R-15). -
Подтверждение ухода с несохранённой заметкой (#42 M-4).
NoteForm.tsx:82: блокrole="alertdialog"без имени, фокус в него не переводится. Решение:- блоку —
aria-label«Уйти без сохранения заметки?»; - при появлении фокус переходит на «Остаться» (безопасное действие по умолчанию);
- прокрутка к блоку остаётся.
- блоку —
-
Связанная задача «нет в зеркале» и окно 404 (#42 M-5).
- В «Связях» (
IssueCard.tsx:123-125) ключ задачи вне зеркала открывает задачу в Jira в новой вкладке (как «Открыть в Jira» в шапке) со значком внешней ссылки, а не окно хаба. Пометка «нет в зеркале» остаётся. - Окно 404 (прямой адрес, ключ из текста, гонка) показывает «Закрыть» (закрывает окно целиком, как ✕). Если окно
открыто переходом из другой карточки (
depth ≥ 1), добавляется «Назад» —navigate(-1)к прежней карточке. - То же «Назад» — у ошибки загрузки с «Повторить».
- В «Связях» (
Строка состояния
-
Ответ опроса прежнего проекта отбрасывается (#116).
ActivityProvider.tsx:- эффект смены проекта (
:75-90) сбрасываетactiveRef/activeи событие; - эффект опроса (
:126-154) в том же коммите видитactiveиз замыкания старого рендера и снова ставит событиеbatch(:139); - после этого
active = null, событие больше никто не снимает.
Решение — эффект опроса обрабатывает ответ, только если отслеживаемый пакет на момент обработки тот же:
activeRef.currentсовпадает сactiveиз замыкания (поidиproject). Иначе ответ игнорируется.finishпо устаревшему ответу тоже не вызывается.Тест — детерминированный, без нагрузки: ответ
/batches/{id}приходит в том же такте, что и смена проекта; после этого событие модуляnull. Существующий тестactivity.test.tsx:331при этом стабилен (прогон в цикле ≥ 20 раз зелёный). - эффект смены проекта (
Лента и карточка
-
Текст комментария в ленте без разметки (#103 С6). Новая чистая функция
wikiPlain(text, opts)рядом с разборщиком (issues/wiki/) переводит вики-разметку в обычный текст тем же разборомparseWiki:- выделение и
{{моно}}— содержимое; - ссылка — её текст (у
[адрес]— адрес); - ключ задачи — ключ,
[~логин]— «@логин», вложение — имя; - блоки — их текст через пробел, заголовок — его текст,
----— пусто.
eventTextдляcommentберётwikiPlain, затем прежние «одна строка» и обрезку до 300 символов с «…». Модуль ленты импортирует функцию из модуля задач того же модуля jira — граница модулей веба не нарушается. Та же функция применяется к «Истории» карточки: она рисуется тем жеEventList. - выделение и
-
Метки видов работ отдельно (#103). В «Прочих полях» (
IssueCard.tsx:227):- строка «Вид работы в Jira» — названия видов из
kind_labels, чьи метки есть у задачи, через запятую, в порядке меток задачи; - строка «Метки» — остальные метки.
Строка с пустым значением, как и сейчас, не показывается. Сравнение меток — точное, как у доски (
dicts.ts). Служебное поле заметки «Вид работы» (work_kind) не трогается. Это заметка хаба, а не метка Jira, и они могут расходиться — отсюда подпись «в Jira». - строка «Вид работы в Jira» — названия видов из
-
Описание из пробелов (#56 M-4).
IssueCard.tsx:79: раздел «Описание» показывается, только еслиdescription.trim()не пуст.
Вики-разметка (#56 M-5, M-6)
-
\#.#добавляется вESCAPABLE(inline.ts:21). Строка\# текстне список (уже так —LISTтребует#первым символом), а\#показывается как#. -
Макросы с параметрами.
MACRO(parse.ts:17) принимает параметры у всех трёх тегов:\{(code|noformat|quote)(?::[^}]*)?\}. Закрывающий тег — по имени без параметров, как сейчас. -
Пробелы после открывающего тега. В
closeMacro(parse.ts:111) остаток строки открывающего тега из одних пробелов в тело блока не попадает. Ведущие пробелы строк внутри блока сохраняются. -
[~логин|текст]. Вbracket(inline.ts:149-152) логин — часть до|, обрезанная. Получается «@логин», подпись не показывается. Пустой логин или логин с пробелом — текст как есть, как сейчас. -
Непарная
[в ячейке таблицы. Вrow(parse.ts:127-132)[увеличивает глубину, только если дальше в строке есть парная]. Иначе[— обычный символ, и|делит ячейки.
Список задач, доска, проекты
-
Список ждёт настройки (#60 M-3). Запрос
GET /issues(useIssues) включается, когда запрос/settingsзавершён — успехом или ошибкой. Пока нет — «Загрузка…», без «По этим фильтрам задач нет». При ошибке настроек закрытых статусов нет, запрос уходит сhide_closed=1, как сейчас. -
Два быстрых переключения фильтра доски (#35 T4.1/M-15).
useBoardPrefs.ts:39-54:toggleиtoggleHiddenсчитают новое значение изprefsзамыкания, и второй клик в том же батче теряет первый. Решение — последнее значение держится вref, обновляемом синхронно вместе сsetPrefs. Новое значение считается изrefи тем же значением пишется вlocalStorage. Чистота updater-функции сохраняется: StrictMode не пишет дважды. -
Фильтр спринтов не из-за эпика (#64 T9.1).
fromHub.ts:30:hasSprint— только по своим задачам, которые не эпики (!isEpic(it) && !it.neighbor && !!it.sprint), то есть по узлам доски. -
«Повторить» при ошибке загрузки проектов (#64 M-6).
ProjectGate.tsx:11: у сообщения «Проекты не загрузились» есть кнопка «Повторить», которая перезапрашиваетGET /projects(refetchзапроса проектов). Через контекст проекта или прямо — на усмотрение исполнителя.
Вложения
-
Прежняя ошибка не мигает после успешного повтора (#86 T6.2). После успешного скачивания хук помнит «успех в этой карточке» для вложения. Строка (
AttachmentList.tsx:36) не показывает серверныйstate = failed, пока данные карточки не перечитаны. Отметку снимает приход данных, гдеstateвложения уже неfailed, или новыйstart. -
401 при опросе скачивания — экран входа (#86 T6.3). В
useAttachmentDownload(:74,:84)AuthErrorидёт вdropTokenOn(e), как в остальном приложении, а не текстом под вложением.HubDownError— текст «хаб не отвечает» под вложением с «Повторить» (требование «Ошибки хаба»). -
Порядок вложений — как у хаба (#86 M-4). Хаб уже отдаёт вложения новыми сверху (
mirror.py:300-301: поcreated_atUTC, затемattachment_id). Веб не пересортирует (newestFirstубирается изAttachmentList).findAttachment(inline.ts:36-45) при одинаковом имени берёт первое вложение в порядке хаба. Так выбор совпадает сattachment_idsхаба и MCP и не зависит отDate.parseстрок Jira.
UI-кит
- Фокус после «Очистить» (#68 T3.3).
SearchInput(ui/inputs.tsx:22): после нажатия «Очистить» фокус переводится в поле ввода (ref). Иначе кнопка исчезает вместе с фокусом.
Risks / Trade-offs
- #116 — гонка в порядке эффектов. Решение опирается на то, что эффект смены проекта стоит раньше эффекта опроса и
синхронно меняет
activeRef. Тест должен воспроизводить именно один коммит со сменой проекта и ответом опроса, иначе он проходит и без правки. Исполнитель проверяет RED. wikiPlainи разбор. Плоский текст строится разбором карточки. Ошибка разбора в ленте даёт исходник — так же безопасно, как в карточке (рендер блока с ошибкой — исходник).- Ссылка «нет в зеркале» в Jira. Адрес Jira — тот же
JIRA_URL, что у «Открыть в Jira». Задача может быть и вне Jira (опечатка в связи) — тогда Jira покажет свою 404, это приемлемо. - Окно 404 и «Назад».
depthберётся из state окна. При прямом адресеdepth = 0— только «Закрыть».
Decisions made without discussion
Все — 25.10.2026: владелец пропустил вопросы brainstorming («делай сам»). Рулинги R-n — с R-1 в пределах фичи.
- R-1 (design #1): при открытии карточки отложенный поиск записывается сразу, окно открывается поверх списка с ним. Обоснование: пользователь ввёл поиск и видит отфильтрованную строку, на которую щёлкнул, — потерять поиск хуже. Цена ошибки — вместо этого отбрасывать поиск: правка одной функции.
- R-2 (design #2): повторное закрытие окна игнорируется до смены адреса. Цена ошибки — нет.
- R-3 (design #3): фокус подтверждения ухода — на «Остаться». Обоснование: безопасное действие по умолчанию, данные не теряются от лишнего Enter. Цена ошибки — перенести фокус на другую кнопку.
- R-4 (design #4): ключ связи «нет в зеркале» открывает Jira в новой вкладке, а не окно хаба с 404. Обоснование: в хабе
про задачу ничего нет, полезное действие — посмотреть её в Jira. Цена ошибки — вернуть окно: одна ветка в
Links. - R-5 (design #4): у окна 404 после перехода по связи — «Назад» (на шаг) и «Закрыть» (всё окно). Цена ошибки — убрать кнопку.
- R-6 (design #5): #116 чинится в провайдере (ответ сверяется с текущим
activeRef), тест не ослабляется. Цена ошибки — нет: гипотеза подтверждена чтением кода при сверке. - R-7 (design #6): лента показывает текст комментария без разметки по разбору карточки; «История» карточки — тоже.
Альтернатива — показывать начало комментария отрендеренным — отвергнута: строка ленты однострочная. Цена ошибки —
правка
eventText. - R-8 (design #7): метки видов работ — отдельной строкой «Вид работы в Jira» с названиями видов, из «Меток» убраны. Альтернатива — просто скрыть. Отвергнута: метка вида — единственный след вида работы в Jira, и он может расходиться с заметкой. Цена ошибки — скрыть строку.
- R-9 (design #12):
[~логин|текст]показывается «@логин» без подписи. Jira такой формы не документирует, логин важнее подписи. Цена ошибки — показывать подпись. - R-10 (design #13): непарная
[в строке таблицы — обычный символ. Цена ошибки — нет. - R-11 (design #14): при ошибке
/settingsсписок всё равно запрашивается (сhide_closed=1, закрытых статусов нет), а не блокируется. Обоснование: список полезнее пустого экрана, ошибку настроек покажет страница. Цена ошибки — нет. - R-12 (design #19):
HubDownErrorпри опросе скачивания остаётся текстом под вложением с «Повторить», отдельного экрана нет — данные карточки остаются на экране (требование «Ошибки хаба»). Цена ошибки — нет. - R-13 (design #20): веб берёт порядок вложений у хаба, а при одинаковых именах — первое в этом порядке. Обоснование:
совпадение с
attachment_idsхаба и MCP. Цена ошибки — вернуть сортировку на клиенте. - R-14 (proposal): в пакет не взяты #42 M-3 (возврат фокуса после закрытия окна), M-6 (
100vh), доступность UI-кита #68 и производительность разбора #56 M-2. Это не ошибки поведения, они остаются в своих issue. Цена ошибки — следующий пакет. - R-15 (design #2, 26.10.2026):
closingKeyвoverlay.tsxсбрасывается и вabortClose(«Остаться»), а не только при сменеlocation.key. Обоснование: при заблокированной навигации ключ записи не меняется, и без сброса второй Esc/✕ после «Остаться» молча игнорировался бы (дефект плана, пойман e2e Task 4). Цена ошибки — повторный ✕ при блокере станет повторно вызывать navigate; безвредно. - R-16 (design #6): в ленте вложение читается именем без списка вложений карточки — флаг
WikiOptions.anyAttachment, выставляется толькоwikiPlain; карточка его не передаёт. Обоснование: финальное ревью I-1,!скрин.png|thumbnail!и[^отчёт.pdf]иначе остаются сырой разметкой. Цена ошибки — правка в одном месте. - R-17: #18 выполнен частично (отметка «успех» не снимается приходом данных) и разбор всего комментария ради 300 символов в ленте — отложены (M-2, M-6 финального ревью). Обоснование: редкий случай и общая тема #56 M-2. Цена ошибки — редкая невидимая ошибка вложения в открытой карточке; задержка ленты на очень длинных комментариях.
- R-18 (design #2): ссылка R-15 указывает на решение 2, а не на несуществующее #22; к решению 2 дописан сброс в
abortClose. Цена ошибки — нет (документация). - R-19: N-1 (пере-ревью финальной волны) — в режиме
anyAttachmentветка!имя!принимает любую пару «!», поэтому в ленте «Срочно! Проверьте! Ок» теряет восклицательные знаки — отложено в follow-up (требовать расширение файла в имени); второй волны правок нет. Цена ошибки — искажённые «!» в тексте строки ленты, данные не страдают. - R-20 (design #6, 26.10.2026, решение владельца по R-19): N-1 исправлена до PR, а не отложена. В режиме
anyAttachment!имя!без списка вложений считается вложением, только если имя — имя файла с расширением без пробелов по краям (FILE_NAMEвinline.ts);[^имя]— явная ссылка на вложение, проверка к ней не применяется. Обоснование: ветка сама внесла искажение «!» в строке ленты, владелец не хочет таких ошибок. Цена ошибки —!слово.слово!в обычном тексте ленты («Ура!Сделано.ру!») прочитается как имя вложения без «!»; данные не страдают.