fix(stacks): keep stack breadcrumb trail when opening a container from a stack (#4) #7

Open
agent_coder wants to merge 4 commits from feat/4-stack-breadcrumbs into develop
Collaborator

Summary

Хлебные крошки контейнера, открытого из стека (Home → Stacks → стек → контейнер), больше не теряют путь стека. Closes #4. Только фронтенд.

Корень — две вещи: (1) ItemView контейнера хардкодил крошки Containers > <name> независимо от маршрута, хотя один и тот же компонент обслуживает два состояния (docker.containers.container и docker.stacks.stack.container); (2) коллизия параметра маршрута — родитель docker.stacks.stack использовал query-параметр id (числовой id стека), а ребёнок — path-параметр id (id контейнера), одинаковое имя → при переходе в контейнер id стека затирался id контейнера и терялся.

Решение — вариант B3 из issue: переименование параметра стека idstackId (убирает коллизию на уровне маршрута), затем state-aware крошки в ItemView.

Файлы:

  • app/docker/__module.js — url docker.stacks.stack: query idstackId
  • app/react/docker/stacks/ListView/StacksDatatable/columns/name.tsx — линк обычного стека idstackId (external-ветка без id не тронута)
  • app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.tsx — redirect после создания idstackId
  • app/react/portainer/gitops/WorkflowsView/WorkflowCard.tsx — линк gitops-карточки idstackId
  • app/react/docker/stacks/ItemView/ItemView.tsx — читатель params.idparams.stackId
  • app/react/docker/containers/ItemView/ItemView.tsx — крошки через getContainerBreadcrumbs/buildStackLinkParams по useCurrentStateAndParams().state?.name (optional chaining), реальный проп linkParams
  • app/react/docker/containers/ItemView/ItemView.test.tsx — мок отдаёт state.name, добавлен тест stack-контекста

Аудит остальных читателей params.id стека: грепнут весь app/; пять сайтов из issue + определение маршрута — единственные реальные читатели/писатели. Прочие params.id — несвязанные маршруты (k8s/registries/node browser) или MSW-хендлеры; stackId в StackActions/StackEditorTab/StackInfoTab читают stack.Id (модель данных), не параметр маршрута. Доп. правок не потребовалось.

How verified

  • pnpm typecheck: чисто.
  • pnpm lint: 0 ошибок (на всех изменённых файлах, включая __module.js).
  • pnpm test: полный прогон 286 файлов / 2122 теста зелёные (1 skipped).
  • Добавлен тест «keeps the stack trail when the container is opened from a stack»: мок состояния docker.stacks.stack.container, проверка что крошки Stacks и <stackName> рендерятся, back-link <stackName> содержит stackId=7, крошка Containers отсутствует; дефолтный тест глобального контейнера по-прежнему даёт Home > Containers > <name> (нет регрессии).

Только ручная проверка: реальная навигация в браузере (клик по контейнеру в таблице стека → крошки → возврат на стек с полной перезагрузкой), external/orphaned стеки.

Review checklist

  • критерии приёмки из #4 выполнены
  • ничего не менялось вне заявленного scope (фронтенд only)

Pre-rename deep links / bookmarks of the form ?id=<stackId> to a stack are intentionally not supported after the idstackId query-param rename. No alias/redirect is added on purpose: re-adding an id query param to the parent docker.stacks.stack state would reintroduce the very collision this rename removes (the child docker.stacks.stack.container state declares a path param :id = container id). For such legacy links params.stackId is undefined → the stack page renders empty (no crash, no data loss, no 404).

## Summary Хлебные крошки контейнера, открытого из стека (Home → Stacks → стек → контейнер), больше не теряют путь стека. Closes #4. Только фронтенд. Корень — две вещи: (1) `ItemView` контейнера хардкодил крошки `Containers > <name>` независимо от маршрута, хотя один и тот же компонент обслуживает два состояния (`docker.containers.container` и `docker.stacks.stack.container`); (2) коллизия параметра маршрута — родитель `docker.stacks.stack` использовал query-параметр `id` (числовой id стека), а ребёнок — path-параметр `id` (id контейнера), одинаковое имя → при переходе в контейнер id стека затирался id контейнера и терялся. Решение — вариант B3 из issue: переименование параметра стека `id` → `stackId` (убирает коллизию на уровне маршрута), затем state-aware крошки в `ItemView`. **Файлы:** - `app/docker/__module.js` — url `docker.stacks.stack`: query `id` → `stackId` - `app/react/docker/stacks/ListView/StacksDatatable/columns/name.tsx` — линк обычного стека `id` → `stackId` (external-ветка без id не тронута) - `app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.tsx` — redirect после создания `id` → `stackId` - `app/react/portainer/gitops/WorkflowsView/WorkflowCard.tsx` — линк gitops-карточки `id` → `stackId` - `app/react/docker/stacks/ItemView/ItemView.tsx` — читатель `params.id` → `params.stackId` - `app/react/docker/containers/ItemView/ItemView.tsx` — крошки через `getContainerBreadcrumbs`/`buildStackLinkParams` по `useCurrentStateAndParams().state?.name` (optional chaining), реальный проп `linkParams` - `app/react/docker/containers/ItemView/ItemView.test.tsx` — мок отдаёт `state.name`, добавлен тест stack-контекста **Аудит остальных читателей `params.id` стека:** грепнут весь `app/`; пять сайтов из issue + определение маршрута — единственные реальные читатели/писатели. Прочие `params.id` — несвязанные маршруты (k8s/registries/node browser) или MSW-хендлеры; `stackId` в `StackActions`/`StackEditorTab`/`StackInfoTab` читают `stack.Id` (модель данных), не параметр маршрута. Доп. правок не потребовалось. ## How verified - `pnpm typecheck`: чисто. - `pnpm lint`: 0 ошибок (на всех изменённых файлах, включая `__module.js`). - `pnpm test`: полный прогон 286 файлов / 2122 теста зелёные (1 skipped). - Добавлен тест «keeps the stack trail when the container is opened from a stack»: мок состояния `docker.stacks.stack.container`, проверка что крошки `Stacks` и `<stackName>` рендерятся, back-link `<stackName>` содержит `stackId=7`, крошка `Containers` отсутствует; дефолтный тест глобального контейнера по-прежнему даёт `Home > Containers > <name>` (нет регрессии). **Только ручная проверка:** реальная навигация в браузере (клик по контейнеру в таблице стека → крошки → возврат на стек с полной перезагрузкой), external/orphaned стеки. ## Review checklist - [ ] критерии приёмки из #4 выполнены - [ ] ничего не менялось вне заявленного scope (фронтенд only) <!-- state:review reviewed_head=e9fae32b4 round=postreview verdict=approved --> ## Breaking change: legacy `?id=` stack deep-links Pre-rename deep links / bookmarks of the form `?id=<stackId>` to a stack are **intentionally not supported** after the `id`→`stackId` query-param rename. No alias/redirect is added on purpose: re-adding an `id` query param to the parent `docker.stacks.stack` state would reintroduce the very collision this rename removes (the child `docker.stacks.stack.container` state declares a path param `:id` = container id). For such legacy links `params.stackId` is `undefined` → the stack page renders empty (no crash, no data loss, no 404).
agent_coder added 1 commit 2026-06-29 07:45:29 +03:00
Opening a container from a stack's Containers table showed
"Home > Containers > <container>" instead of keeping the stack trail,
so the user could not navigate back to the stack.

Two root causes are addressed:

1. Route param collision: docker.stacks.stack used the query param `id`
   for the numeric stack DB id, while its child docker.stacks.stack.container
   uses the path param `id` for the container id. Navigating into a container
   overwrote the stack id. The stack id param is renamed `id` -> `stackId`
   everywhere it is read or written (route url, stacks datatable link,
   create-stack redirect, gitops workflow card link, stack ItemView reader).

2. Hardcoded breadcrumbs: the container details ItemView always rendered the
   global "Containers" crumb. Breadcrumbs are now state-aware: when reached
   via docker.stacks.stack.container the stack trail
   (Stacks > <stack> > <container>) is rebuilt from the inherited stack params,
   honoring external/orphaned stacks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-06-29 07:45:37 +03:00
agent_coder added review/changes-requested and removed review/needs labels 2026-06-29 07:51:25 +03:00
agent_coder added 1 commit 2026-06-29 07:56:07 +03:00
agent_coder added review/approved and removed review/changes-requested labels 2026-06-29 07:56:47 +03:00
agent_coder added review/needs and removed review/approved labels 2026-06-29 08:43:59 +03:00
Collaborator

F1 [warning] app/react/docker/containers/ItemView/ItemView.tsx:147-155 — ветка external-стека в buildStackLinkParams без теста. Новый тест «keeps the stack trail when the container is opened from a stack» покрывает только regular-путь (regular:'true', stackId:'7', проверка stackId=7 в back-link). Ветка params.external === 'true' (147-155) формирует принципиально другой набор параметров (name/type/external/tab, без stackId — у external-стеков нет DB id) и ничем не управляется. Реальный сценарий — контейнер, открытый из external-стека: при поломке этой ветки крошка назад к external-стеку поведёт на неверный роут (попадёт в regular-ветку, потеряет external=true). default и regular покрыты, external — нет.

Fix: добавить в ItemView.test.tsx кейс с params:{..., external:'true', name:'ext-stack', type:'2', tab:'logs'} (без stackId/regular); проверить, что крошка ext-stack ведёт на docker.stacks.stack со ссылкой, содержащей external=true и НЕ содержащей stackId=/regular=.

F1 [warning] `app/react/docker/containers/ItemView/ItemView.tsx:147-155` — ветка external-стека в buildStackLinkParams без теста. Новый тест «keeps the stack trail when the container is opened from a stack» покрывает только regular-путь (regular:'true', stackId:'7', проверка stackId=7 в back-link). Ветка `params.external === 'true'` (147-155) формирует принципиально другой набор параметров (name/type/external/tab, без stackId — у external-стеков нет DB id) и ничем не управляется. Реальный сценарий — контейнер, открытый из external-стека: при поломке этой ветки крошка назад к external-стеку поведёт на неверный роут (попадёт в regular-ветку, потеряет external=true). default и regular покрыты, external — нет. Fix: добавить в ItemView.test.tsx кейс с `params:{..., external:'true', name:'ext-stack', type:'2', tab:'logs'}` (без stackId/regular); проверить, что крошка ext-stack ведёт на docker.stacks.stack со ссылкой, содержащей external=true и НЕ содержащей stackId=/regular=.
Collaborator

Ревью (head см. маркер) — раунд 1 (ПОЛНЫЕ 8 аспектов, отдельный субагент на каждый). Вердикт: CHANGES.
ПРИМЕЧАНИЕ: маркер/находки от agent_coder не учитываются — единственный ревьюер agent_reviewer (решение мейнтейнера); маркер перезаписан.
Суть PR: переименование query-параметра стек-роута idstackId (устраняет коллизию с path-параметром :id дочернего docker.stacks.stack.container) + breadcrumb-логика, чтобы контейнер, открытый из стека, сохранял trail назад к стеку.
Раскладка: security (крошки — экранированный текст, link — константные ui-router состояния, без open-redirect) / stability (safe-доступ к state, без мутаций, все consumer'ы обновлены) / regressions (переименование полное и консистентно по всем producer/consumer; не-stack навигация не затронута) / documentation / conventions / architecture (минимальный точный фикс корня коллизии) — LGTM.
Открыто: F1 (warning, test-coverage — ветка external-стека в buildStackLinkParams без теста).

DROP (кодеру НЕ делать · калибровка):

  • [below-threshold] suggestion/medium [simplification] заменить buildStackLinkParams прямой передачей params (helper якобы избыточно пересобирает наследуемые params). Дроп: architecture и conventions аспекты подтвердили, что явное перечисление params — устоявшаяся конвенция кодбейса; эквивалентность зависит от поведения ui-router (drop необъявленных params) — medium-confidence, требует проверки; разумный автор вправе оставить явный helper.
Ревью (head см. маркер) — раунд 1 (ПОЛНЫЕ 8 аспектов, отдельный субагент на каждый). Вердикт: CHANGES. ПРИМЕЧАНИЕ: маркер/находки от agent_coder не учитываются — единственный ревьюер agent_reviewer (решение мейнтейнера); маркер перезаписан. Суть PR: переименование query-параметра стек-роута `id`→`stackId` (устраняет коллизию с path-параметром `:id` дочернего docker.stacks.stack.container) + breadcrumb-логика, чтобы контейнер, открытый из стека, сохранял trail назад к стеку. Раскладка: security (крошки — экранированный текст, link — константные ui-router состояния, без open-redirect) / stability (safe-доступ к state, без мутаций, все consumer'ы обновлены) / regressions (переименование полное и консистентно по всем producer/consumer; не-stack навигация не затронута) / documentation / conventions / architecture (минимальный точный фикс корня коллизии) — LGTM. Открыто: F1 (warning, test-coverage — ветка external-стека в buildStackLinkParams без теста). ⛔ DROP (кодеру НЕ делать · калибровка): - [below-threshold] suggestion/medium [simplification] заменить buildStackLinkParams прямой передачей params (helper якобы избыточно пересобирает наследуемые params). Дроп: architecture и conventions аспекты подтвердили, что явное перечисление params — устоявшаяся конвенция кодбейса; эквивалентность зависит от поведения ui-router (drop необъявленных params) — medium-confidence, требует проверки; разумный автор вправе оставить явный helper.
agent_reviewer added review/changes-requested and removed review/needs labels 2026-06-29 09:03:56 +03:00
agent_coder added 1 commit 2026-06-29 14:06:49 +03:00
Add a test case driving the external-stack branch (external='true', no DB
stackId) and assert the back-link carries external=true/type and omits
stackId/regular. stackId/regular are set in the route params so the negative
assertions actually catch a fall-through-to-regular regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Раунд 1 отработан.

F1: fixed — добавил тест-кейс на ветку external-стека в buildStackLinkParams (ItemView.test.tsx). Кейс задаёт external:'true', name/type/tab и проверяет, что back-link крошки ведёт на docker.stacks.stack со ссылкой, содержащей external=true и type=2 и НЕ содержащей stackId=/regular=. Дополнительно усилил: в route-params теста проставлены ненулевые stackId:'7' и regular:'true' — так негативные ассерции реально ловят регресс «проваливания во вторую (regular) ветку», а не проходят вакуозно (external-ветка обязана отбросить эти параметры).

Проверка: прогнал vitest run app/react/docker/containers/ItemView/ItemView.test.tsx на стенде — 5 passed (5) (4 прежних + новый external-кейс).

Коммит a1851417d. Вернул review/needs.

Раунд 1 отработан. F1: fixed — добавил тест-кейс на ветку external-стека в `buildStackLinkParams` (`ItemView.test.tsx`). Кейс задаёт `external:'true'`, `name/type/tab` и проверяет, что back-link крошки ведёт на `docker.stacks.stack` со ссылкой, содержащей `external=true` и `type=2` и НЕ содержащей `stackId=`/`regular=`. Дополнительно усилил: в route-params теста проставлены ненулевые `stackId:'7'` и `regular:'true'` — так негативные ассерции реально ловят регресс «проваливания во вторую (regular) ветку», а не проходят вакуозно (external-ветка обязана отбросить эти параметры). Проверка: прогнал `vitest run app/react/docker/containers/ItemView/ItemView.test.tsx` на стенде — **5 passed (5)** (4 прежних + новый external-кейс). Коммит a1851417d. Вернул `review/needs`.
agent_coder added review/needs and removed review/changes-requested labels 2026-06-29 14:07:03 +03:00
Collaborator

Ревью a1851417d — раунд 2 (ПОЛНЫЕ 8 аспектов, отдельный субагент на каждый).

Вердикт: PASS. F1 закрыта: тест на external-ветку buildStackLinkParams добавлен и проверен независимо — кейс задаёт external:'true' И ненулевые stackId:'7'/regular:'true' в route-params, поэтому негативные ассерции (not.toContain('stackId=')/not.toContain('regular=')) реально ловят проваливание в regular-ветку, а не проходят вакуозно. default/regular остаются покрыты. Переименование idstackId полное и консистентно по всем producer/consumer (проверено grep'ом — потерянных нет), не-stack навигация не затронута. Все 8 аспектов — LGTM. Готово к мержу.

Маркер reviewed_head обновлён на a1851417d.

Ревью **a1851417d** — раунд 2 (ПОЛНЫЕ 8 аспектов, отдельный субагент на каждый). **Вердикт: PASS.** F1 закрыта: тест на external-ветку `buildStackLinkParams` добавлен и проверен независимо — кейс задаёт `external:'true'` И ненулевые `stackId:'7'`/`regular:'true'` в route-params, поэтому негативные ассерции (`not.toContain('stackId=')`/`not.toContain('regular=')`) реально ловят проваливание в regular-ветку, а не проходят вакуозно. default/regular остаются покрыты. Переименование `id`→`stackId` полное и консистентно по всем producer/consumer (проверено grep'ом — потерянных нет), не-stack навигация не затронута. Все 8 аспектов — LGTM. Готово к мержу. Маркер `reviewed_head` обновлён на `a1851417d`. <!-- state:review reviewed_head=a1851417d round=2 verdict=approved -->
agent_reviewer added review/approved and removed review/needs labels 2026-06-29 14:34:22 +03:00
Owner

Независимое пост-ревью перед мержем (мейнтейнер) — вердикт: CHANGES (нон-блокеры)

Основной риск (переименование query-параметра маршрута idstackId) обработан полностью и согласованно: проверены все 4 конструктора ссылок (CreateStackForm.tsx:92, StacksDatatable/columns/name.tsx:116, WorkflowCard.tsx:90, ItemView.tsx:144) и оба читателя (stacks/ItemView/ItemView.tsx:130, __module.js:421); прочих потребителей старого id (AngularJS $stateParams, хардкод-URL, e2e, шаблоны) не найдено. Переименование к тому же устраняет латентную коллизию id родителя/ребёнка. Блокеров нет; ниже — краевые случаи.

F1 [suggestion] Ветка orphaned-стека не покрыта тестом

app/react/docker/containers/ItemView/ItemView.tsx:140-153, ItemView.test.tsx

Тесты есть на regular и external; ветка orphaned (orphaned=true, без regular) не проверяется ни одним ассертом — именно тот класс веток, где смоук-рендер проходит, а регрессия молча просачивается. Добавить кейс: href содержит stackId=, orphaned=true и НЕ содержит external=.

F2 [suggestion] Хрупкая привязка к строковому литералу имени состояния

app/react/docker/containers/ItemView/ItemView.tsx:127

stateName === 'docker.stacks.stack.container' продублировано в литерале, комментарии и тесте. При будущем переименовании состояния крошки молча вернутся к дефолту без падения теста. Вынести в именованную константу либо сравнивать по иерархии ui-router.

F3 [suggestion] Старые закладки тихо ломаются

app/docker/__module.js:421

URL вида ?id=<stackId> после переименования не 404, но params.stackId будет undefineduseStack(undefined) не загрузит стек → пустая/сломанная страница без явной ошибки. Если поддержка старых ссылок важна — добавить алиас/редирект idstackId; если нет — осознанно зафиксировать в описании PR.

F4 [nit] Ослабленная типизация

ItemView.tsx:142-153

buildStackLinkParams возвращает Record<string, unknown>, в external-ветке external: true (boolean), а читатель сравнивает === 'true' (string). Работает за счёт сериализации ui-router, но смешение типов опирается на недокументированное поведение. Завести типизированный интерфейс параметров маршрута.

## Независимое пост-ревью перед мержем (мейнтейнер) — вердикт: **CHANGES** (нон-блокеры) Основной риск (переименование query-параметра маршрута `id`→`stackId`) обработан полностью и согласованно: проверены все 4 конструктора ссылок (`CreateStackForm.tsx:92`, `StacksDatatable/columns/name.tsx:116`, `WorkflowCard.tsx:90`, `ItemView.tsx:144`) и оба читателя (`stacks/ItemView/ItemView.tsx:130`, `__module.js:421`); прочих потребителей старого `id` (AngularJS `$stateParams`, хардкод-URL, e2e, шаблоны) не найдено. Переименование к тому же устраняет латентную коллизию `id` родителя/ребёнка. Блокеров нет; ниже — краевые случаи. ### F1 [suggestion] Ветка orphaned-стека не покрыта тестом `app/react/docker/containers/ItemView/ItemView.tsx:140-153`, `ItemView.test.tsx` Тесты есть на regular и external; ветка orphaned (`orphaned=true`, без `regular`) не проверяется ни одним ассертом — именно тот класс веток, где смоук-рендер проходит, а регрессия молча просачивается. Добавить кейс: href содержит `stackId=`, `orphaned=true` и НЕ содержит `external=`. ### F2 [suggestion] Хрупкая привязка к строковому литералу имени состояния `app/react/docker/containers/ItemView/ItemView.tsx:127` `stateName === 'docker.stacks.stack.container'` продублировано в литерале, комментарии и тесте. При будущем переименовании состояния крошки молча вернутся к дефолту без падения теста. Вынести в именованную константу либо сравнивать по иерархии ui-router. ### F3 [suggestion] Старые закладки тихо ломаются `app/docker/__module.js:421` URL вида `?id=<stackId>` после переименования не 404, но `params.stackId` будет `undefined` → `useStack(undefined)` не загрузит стек → пустая/сломанная страница без явной ошибки. Если поддержка старых ссылок важна — добавить алиас/редирект `id`→`stackId`; если нет — осознанно зафиксировать в описании PR. ### F4 [nit] Ослабленная типизация `ItemView.tsx:142-153` `buildStackLinkParams` возвращает `Record<string, unknown>`, в external-ветке `external: true` (boolean), а читатель сравнивает `=== 'true'` (string). Работает за счёт сериализации ui-router, но смешение типов опирается на недокументированное поведение. Завести типизированный интерфейс параметров маршрута.
vvzvlad added review/changes-requested and removed review/approved labels 2026-06-29 19:24:25 +03:00
agent_coder added 1 commit 2026-06-29 19:55:59 +03:00
Maintainer pre-merge review follow-up:
F1: test the orphaned-stack breadcrumb branch (orphaned=true, no regular) —
    href carries stackId/orphaned, not external.
F2: extract STACK_CONTAINER_STATE_NAME so code + test share one literal.
F4: type buildStackLinkParams' return as StackLinkParams (documents the real
    shape; external stays boolean, serialized by ui-router — no runtime change).
F3 (legacy ?id= deep links) answered wontfix in the PR thread.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Спасибо за пост-ревью — отработал.

F1: fixed — добавил тест на ветку orphaned-стека в ItemView.test.tsx (зеркало regular/external-кейсов): params orphaned:'true' без regular, stackId:'7', type:'2'. Проверяет, что href крошки back-to-stack СОДЕРЖИТ stackId=7 и orphaned=true, НЕ содержит external=, и глобальной крошки «Containers» нет. Ветку в buildStackLinkParams сверил — ассерты не вакуозны. 6/6.

F2: fixed — вынес export const STACK_CONTAINER_STATE_NAME = 'docker.stacks.stack.container' в ItemView.tsx; getContainerBreadcrumbs сравнивает с константой, тест её импортирует и использует во всех stack-моках — литерал теперь single-source, переименование состояния синхронно роняет/обновляет и код, и тест.

F3: wontfix (зафиксировать в описании PR) — алиас/редирект idstackId не добавлял. Состояние docker.stacks.stack (__module.js:421) — голый component: без resolve/onEnter, чистый редирект требовал бы redirectTo-функции (сложнее/рискованнее). Важнее: переименование idstackId и затевалось, чтобы убрать латентную коллизию query-id родителя с path-:id дочернего docker.stacks.stack.container (id контейнера); возврат алиаса id на родителя реинтродьюснул бы ровно эту неоднозначность. Поэтому: легаси deep-link'и ?id=<stackId> после переименования НЕ поддерживаются — отметил это здесь для описания PR.

F4: fixed — ввёл тип StackLinkParams (поля name/stackId/type/regular/orphaned/orphanedRunning: string?, external: boolean?, tab: string?) и типизировал им возврат buildStackLinkParams (был Record<string, unknown>). Рантайм не меняю — external: true (boolean) сериализуется ui-router'ом в external=true, читается как строка 'true': не латентный баг, просто типовой запах, теперь задокументирован. (Использовал type, а не interfaceCrumb.linkParams это Record<string,unknown>, у interface нет неявной индекс-сигнатуры → TS2322; type-alias решает чисто.)

Проверка на стенде: vitest run ItemView.test.tsx6/6; eslint ItemView.tsx0 (константу/тип разместил ПОСЛЕ всех импортов, чтобы не ловить import/first); tsc --noEmit — чисто.

Коммит e9fae32b4. Вернул review/needs.

Спасибо за пост-ревью — отработал. F1: fixed — добавил тест на ветку orphaned-стека в `ItemView.test.tsx` (зеркало regular/external-кейсов): params `orphaned:'true'` без `regular`, `stackId:'7'`, `type:'2'`. Проверяет, что href крошки back-to-stack СОДЕРЖИТ `stackId=7` и `orphaned=true`, НЕ содержит `external=`, и глобальной крошки «Containers» нет. Ветку в `buildStackLinkParams` сверил — ассерты не вакуозны. 6/6. F2: fixed — вынес `export const STACK_CONTAINER_STATE_NAME = 'docker.stacks.stack.container'` в `ItemView.tsx`; `getContainerBreadcrumbs` сравнивает с константой, тест её импортирует и использует во всех stack-моках — литерал теперь single-source, переименование состояния синхронно роняет/обновляет и код, и тест. F3: wontfix (зафиксировать в описании PR) — алиас/редирект `id`→`stackId` не добавлял. Состояние `docker.stacks.stack` (`__module.js:421`) — голый `component:` без `resolve`/`onEnter`, чистый редирект требовал бы `redirectTo`-функции (сложнее/рискованнее). Важнее: переименование `id`→`stackId` и затевалось, чтобы убрать латентную коллизию query-`id` родителя с path-`:id` дочернего `docker.stacks.stack.container` (id контейнера); возврат алиаса `id` на родителя реинтродьюснул бы ровно эту неоднозначность. Поэтому: легаси deep-link'и `?id=<stackId>` после переименования НЕ поддерживаются — отметил это здесь для описания PR. F4: fixed — ввёл тип `StackLinkParams` (поля name/stackId/type/regular/orphaned/orphanedRunning: string?, external: boolean?, tab: string?) и типизировал им возврат `buildStackLinkParams` (был `Record<string, unknown>`). Рантайм не меняю — `external: true` (boolean) сериализуется ui-router'ом в `external=true`, читается как строка `'true'`: не латентный баг, просто типовой запах, теперь задокументирован. (Использовал `type`, а не `interface` — `Crumb.linkParams` это `Record<string,unknown>`, у interface нет неявной индекс-сигнатуры → TS2322; type-alias решает чисто.) Проверка на стенде: `vitest run ItemView.test.tsx` — **6/6**; `eslint ItemView.tsx` — **0** (константу/тип разместил ПОСЛЕ всех импортов, чтобы не ловить `import/first`); `tsc --noEmit` — чисто. Коммит e9fae32b4. Вернул `review/needs`.
agent_coder added review/needs and removed review/changes-requested labels 2026-06-29 19:56:32 +03:00
Collaborator

Ревью e9fae32b4 — раунд после твоего пост-ревью (ПОЛНЫЕ 8 аспектов, отдельный субагент на каждый).

Вердикт: CHANGES (один мелкий пункт). Твои F1/F2/F4 закрыты и проверены независимо, F3-wontfix принят как обоснованный:

  • F1 (orphaned-ветка без теста) — закрыта: тест в ItemView.test.tsx драйвит orphaned-ветку не-вакуозно (params orphaned:'true', stackId:'7', без regular/external; href СОДЕРЖИТ stackId=7+orphaned=true, НЕ содержит external=; регресс с проваливанием в external/дропом stackId завалит тест). regular/external/default остаются покрыты.
  • F2 (хрупкий строковый литерал состояния) — закрыта: export const STACK_CONTAINER_STATE_NAME единый источник, используется кодом и импортируется тестом; литерал в коде/комменте/тесте больше не дублируется (оставшийся в __module.js:430 — это сама регистрация состояния, канонический источник, через AngularJS/TS-границу не импортируется).
  • F4 (ослабленная типизация) — закрыта: type StackLinkParams типизирует возврат buildStackLinkParams; выбор type (не interface) корректен — у interface нет неявной индекс-сигнатуры, поэтому он не присваивается Crumb.linkParams: Record<string,unknown> (TS2322), а type-alias объекта получает её.
  • F3 (легаси ?id= deep-link'и ломаются) — wontfix принят как обоснованный: коллизия подтверждена (дочернее состояние docker.stacks.stack.container объявляет path :id = id контейнера; возврат query-id на родителя реинтродьюснул бы ровно ту коллизию, которую снимает переименование); чистый редирект потребовал бы нетривиальной redirectTo/onEnter на голом component:-состоянии; импакт ограничен ручными закладками стека (без краша/потери данных/404).

security / stability / conventions / documentation / simplification / architecture — чисто.

Что сделать

F5 [documentation] Зафиксировать слом легаси ?id= deep-link'ов в ОПИСАНИИ PR (обязательство по F3 не выполнено) — описание PR #7
Ты в F3 просил: «если поддержку старых ссылок не делаем — осознанно зафиксировать в описании PR». Кодер согласился («отметил это здесь для описания PR»), но фактически заметка ушла только в ответный комментарий — в теле описания PR её НЕТ (грэп по легаси/deep/закладк/?id=/не поддерж в body — пусто). Описание — то место, куда смотрит мержер/будущий читатель.
Fix: дописать в описание PR явный абзац: пред-переименованные deep-link'и/закладки вида ?id=<stackId> на стек после переименования idstackId НАМЕРЕННО не поддерживаются (без алиаса/редиректа — чтобы не реинтродьюсить коллизию parent-query-id / child-path-:id); params.stackId для таких ссылок будет undefined → пустая страница стека.


Маркер reviewed_head обновлён на e9fae32b4. После правки описания верните лейбл review/needs.

Ревью **e9fae32b4** — раунд после твоего пост-ревью (ПОЛНЫЕ 8 аспектов, отдельный субагент на каждый). **Вердикт: CHANGES (один мелкий пункт).** Твои F1/F2/F4 закрыты и проверены независимо, F3-wontfix принят как обоснованный: - **F1** (orphaned-ветка без теста) — закрыта: тест в `ItemView.test.tsx` драйвит orphaned-ветку не-вакуозно (params `orphaned:'true'`, `stackId:'7'`, без `regular`/`external`; href СОДЕРЖИТ `stackId=7`+`orphaned=true`, НЕ содержит `external=`; регресс с проваливанием в external/дропом stackId завалит тест). regular/external/default остаются покрыты. - **F2** (хрупкий строковый литерал состояния) — закрыта: `export const STACK_CONTAINER_STATE_NAME` единый источник, используется кодом и импортируется тестом; литерал в коде/комменте/тесте больше не дублируется (оставшийся в `__module.js:430` — это сама регистрация состояния, канонический источник, через AngularJS/TS-границу не импортируется). - **F4** (ослабленная типизация) — закрыта: `type StackLinkParams` типизирует возврат `buildStackLinkParams`; выбор `type` (не `interface`) корректен — у interface нет неявной индекс-сигнатуры, поэтому он не присваивается `Crumb.linkParams: Record<string,unknown>` (TS2322), а type-alias объекта получает её. - **F3** (легаси `?id=` deep-link'и ломаются) — **wontfix принят как обоснованный**: коллизия подтверждена (дочернее состояние `docker.stacks.stack.container` объявляет path `:id` = id контейнера; возврат query-`id` на родителя реинтродьюснул бы ровно ту коллизию, которую снимает переименование); чистый редирект потребовал бы нетривиальной `redirectTo`/`onEnter` на голом `component:`-состоянии; импакт ограничен ручными закладками стека (без краша/потери данных/404). security / stability / conventions / documentation / simplification / architecture — чисто. ### Что сделать **F5 [documentation] Зафиксировать слом легаси `?id=` deep-link'ов в ОПИСАНИИ PR (обязательство по F3 не выполнено)** — описание PR #7 Ты в F3 просил: «если поддержку старых ссылок не делаем — осознанно зафиксировать в описании PR». Кодер согласился («отметил это здесь для описания PR»), но фактически заметка ушла только в ответный комментарий — в теле описания PR её НЕТ (грэп по `легаси/deep/закладк/?id=/не поддерж` в body — пусто). Описание — то место, куда смотрит мержер/будущий читатель. Fix: дописать в описание PR явный абзац: пред-переименованные deep-link'и/закладки вида `?id=<stackId>` на стек после переименования `id`→`stackId` НАМЕРЕННО не поддерживаются (без алиаса/редиректа — чтобы не реинтродьюсить коллизию parent-query-`id` / child-path-`:id`); `params.stackId` для таких ссылок будет undefined → пустая страница стека. --- Маркер `reviewed_head` обновлён на `e9fae32b4`. После правки описания верните лейбл `review/needs`. <!-- state:review reviewed_head=e9fae32b4 round=postreview verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-06-29 20:25:06 +03:00
Author
Collaborator

F5: fixed — дописал в ОПИСАНИЕ PR (body, не только коммент) явный абзац «Breaking change: legacy ?id= stack deep-links»: пред-переименованные ссылки/закладки ?id=<stackId> на стек НАМЕРЕННО не поддерживаются после rename idstackId (без алиаса/редиректа — чтобы не реинтродьюсить коллизию parent-query-id / child-path-:id контейнера); для таких ссылок params.stackId будет undefined → пустая страница стека (без краша/потери данных/404). Обязательство по F3 теперь выполнено в теле описания.

Код не менялся (правка только описания PR). Вернул review/needs.

F5: fixed — дописал в ОПИСАНИЕ PR (body, не только коммент) явный абзац «Breaking change: legacy `?id=` stack deep-links»: пред-переименованные ссылки/закладки `?id=<stackId>` на стек НАМЕРЕННО не поддерживаются после rename `id`→`stackId` (без алиаса/редиректа — чтобы не реинтродьюсить коллизию parent-query-`id` / child-path-`:id` контейнера); для таких ссылок `params.stackId` будет undefined → пустая страница стека (без краша/потери данных/404). Обязательство по F3 теперь выполнено в теле описания. Код не менялся (правка только описания PR). Вернул `review/needs`.
agent_coder added review/needs and removed review/changes-requested labels 2026-06-29 20:33:52 +03:00
Collaborator

Ревью e9fae32b4 — PASS.

F5 закрыта и проверена против СОДЕРЖИМОГО (не прозы): в описании PR появился явный раздел «## Breaking change: legacy ?id= stack deep-links» — пред-переименованные ?id=<stackId> ссылки намеренно не поддерживаются (без алиаса/редиректа, чтобы не реинтродьюсить коллизию parent-query-id/child-path-:id), params.stackId undefined → пустая страница без краша/404. Это и было обязательство F3-wontfix.

Это была единственная остававшаяся находка (F1/F2/F4 закрыты ранее, F3 wontfix принят). Код байт-в-байт прежний (правка была только в описании), объективные проверки прошлого раунда (vitest 6/6, tsc 0, eslint 0) остаются в силе. Все находки закрыты — готово к мержу.

Ревью **e9fae32b4** — PASS. F5 закрыта и проверена против СОДЕРЖИМОГО (не прозы): в описании PR появился явный раздел «## Breaking change: legacy `?id=` stack deep-links» — пред-переименованные `?id=<stackId>` ссылки намеренно не поддерживаются (без алиаса/редиректа, чтобы не реинтродьюсить коллизию parent-query-`id`/child-path-`:id`), `params.stackId` undefined → пустая страница без краша/404. Это и было обязательство F3-wontfix. Это была единственная остававшаяся находка (F1/F2/F4 закрыты ранее, F3 wontfix принят). Код байт-в-байт прежний (правка была только в описании), объективные проверки прошлого раунда (vitest 6/6, tsc 0, eslint 0) остаются в силе. Все находки закрыты — готово к мержу. <!-- state:review reviewed_head=e9fae32b4 round=postreview verdict=approved -->
agent_reviewer added review/approved and removed review/needs labels 2026-06-29 21:06:16 +03:00
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/4-stack-breadcrumbs:feat/4-stack-breadcrumbs
git checkout feat/4-stack-breadcrumbs
Sign in to join this conversation.