fix(gre): сохранять меню действий при обновлении таблицы

This commit is contained in:
Denozordec committed 2026-10-08 16:27:17 +07:00
1 parent 26e8225815
commit 731803bade
7 files changed
+170 -85

No files matched your search

+92 -80
View File
@@ -2,7 +2,7 @@
import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useEffect, useMemo } from "react"
import { createContext, useContext, useEffect, useMemo } from "react"
import {
type ColumnDef,
useTable,
@@ -111,6 +111,82 @@ interface GreTunnelsDataGridProps {
emptyDescription?: string
}
type GreActions = Pick<GreTunnelsDataGridProps, "onCodePreview" | "onEdit" | "onToggle" | "onDelete" | "mutationsLocked" | "canManage">
const GreActionsContext = createContext<GreActions | null>(null)
// Keep the cell component identity stable while permissions and callbacks update.
function GreActionsCell({ row }: { row: { original: GreTunnel } }) {
const actions = useContext(GreActionsContext)
if (!actions) throw new Error("GRE actions context is missing")
const { onCodePreview, onEdit, onToggle, onDelete, mutationsLocked, canManage } = actions
const t = row.original
return (
<div className="flex items-center gap-1 justify-end">
<Button
type="button"
variant="ghost"
size="icon"
className={cn(
"size-11 shrink-0",
)}
title="Предпросмотр кода RouterOS"
aria-label={`Просмотр кода: ${t.name}`}
onClick={(e) => {
e.stopPropagation()
onCodePreview(t)
}}
>
<CodeXmlIcon className="size-3.5" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="outline"
size="icon"
className={cn(
"size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
"opacity-100",
"data-popup-open:opacity-100",
)}
aria-label={`Действия: ${t.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuGroup>
<DropdownMenuLabel>{t.name}</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onCodePreview(t)}>
<CodeXmlIcon className="size-4" /> Просмотр кода
</DropdownMenuItem>
<DropdownMenuItem disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onEdit} onClick={() => onEdit?.(t)}>
<PencilIcon className="size-4" /> Редактировать
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onToggle} onClick={() => onToggle?.(t)}>
<PowerIcon className="size-4" />
{t.enabled ? "Выключить" : "Включить"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onDelete}
onClick={() => onDelete?.(t)}
>
<Trash2Icon className="size-4" /> Удалить туннель
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
function GreTunnelsDataGrid({
tunnels,
servers,
@@ -281,78 +357,12 @@ function GreTunnelsDataGrid({
id: "actions",
header: () => <span className="sr-only">Действия</span>,
enableSorting: false,
cell: ({ row }) => {
const t = row.original
return (
<div className="flex items-center gap-1 justify-end">
<Button
type="button"
variant="ghost"
size="icon"
className={cn(
"size-11 shrink-0",
)}
title="Предпросмотр кода RouterOS"
aria-label={`Просмотр кода: ${t.name}`}
onClick={(e) => {
e.stopPropagation()
onCodePreview(t)
}}
>
<CodeXmlIcon className="size-3.5" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="outline"
size="icon"
className={cn(
"size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
"opacity-100",
"data-popup-open:opacity-100",
)}
aria-label={`Действия: ${t.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuGroup>
<DropdownMenuLabel>{t.name}</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onCodePreview(t)}>
<CodeXmlIcon className="size-4" /> Просмотр кода
</DropdownMenuItem>
<DropdownMenuItem disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onEdit} onClick={() => onEdit?.(t)}>
<PencilIcon className="size-4" /> Редактировать
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onToggle} onClick={() => onToggle?.(t)}>
<PowerIcon className="size-4" />
{t.enabled ? "Выключить" : "Включить"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onDelete}
onClick={() => onDelete?.(t)}
>
<Trash2Icon className="size-4" /> Удалить туннель
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
},
cell: GreActionsCell,
size: 112,
meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST },
},
],
[onCodePreview, onEdit, onToggle, onDelete, mutationsLocked, canManage, unknownServerIds, poolMap, serverMap],
[unknownServerIds, poolMap, serverMap],
)
const table = useTable({
@@ -383,17 +393,19 @@ function GreTunnelsDataGrid({
}
return (
<DataGridShell
table={table}
recordCount={tunnels.length}
pagination={tunnels.length > 50 || pageSize !== 50}
tableLayout={{ dense: true, columnsPinnable: true }}
tableClassNames={{ headerRow: "border-b border-border", bodyRow: "group/row" }}
>
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
<GreActionsContext.Provider value={{ onCodePreview, onEdit, onToggle, onDelete, mutationsLocked, canManage }}>
<DataGridShell
table={table}
recordCount={tunnels.length}
pagination={tunnels.length > 50 || pageSize !== 50}
tableLayout={{ dense: true, columnsPinnable: true }}
tableClassNames={{ headerRow: "border-b border-border", bodyRow: "group/row" }}
>
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
</GreActionsContext.Provider>
)
}
+8
View File
@@ -377,3 +377,11 @@ gre-comment.test.ts проверил create plan→native comment→map→snapsh
Первые browser попытки остановились на меню, которое исчезало при перерисовке. Таймер первой пары удалил собственные объекты. Немедленная сверка /ip/route дала mismatch; финальный harness добавил ограниченное ожидание сходимости маршрутов, не меняя их вручную. Итоговый запуск exit0: DELETE200/200, все выбранные поля GRE/IP/routes/IPv4 Firewall совпали с baseline; CHR1 потребовал одно повторное чтение через250ms, CHR2 совпал сразу. Тестовое меню в успешном проходе пришлось открыть повторно1раз: это не исправленный UX, а отдельная выявленная недоработка для54. Проверка не является доказательством неизменности всех runtime параметров/полного export.
Own GRE интерфейсы, адреса и connected route удалены; эфемерный контейнер и туннель8018 закрыты, остались три основные Compose-службы. Main controls до/после: servers2/speedHistory9/retention14/migrations9 совпадают; GRE audit revisions намеренно сохранены. Следующий12.5.54 — сохранить устойчивость меню действий GRE при перерисовке и проверить профильный сценарий; затем продолжать настоящие VPN объекты. Права, частичные отказы остальных VPN, Portal/Telegram и прочие внешние ограничения остаются открытыми. Локальный коммит без push.
## 12.5.54 — устойчивость меню GRE при обновлении таблицы (2026-10-08)
При обновлении callbacks/прав пересоздавался inline renderer actions column; React монтировал ячейку заново и закрывал Base UI меню. В components/data-grids/gre-tunnels-data-grid.tsx вынесен стабильный GreActionsCell, текущие callbacks/permissions передаются через типизированный React context. ReUI composition, оформление, API таблицы и ключ строки serverId/id сохранены. Политика блокировки операций при загрузке и отсутствии доступа сохранена.
Проверки: новый tests/e2e/gre-menu-refresh.spec.ts открывает меню, запускает GET без outside pointer event, проверяет сохранение меню, disabled во время загрузки, восстановление enabled и свежий peer IP в форме. На прежнем HEAD тест падает именно из-за исчезнувшего menuitem; исправленный код проходит. Вместе с gre-toggle.spec.ts — 3/3 passed. Scoped TypeScript (tmp/tsconfig-speed-check.json) и ESLint изменённых файлов passed. Первую фикстуру скорректировали: инициализация источника допускает несколько GET, задержка включается только при проверяемом refresh. ReUI data-grid checklist проверен, общие компоненты не менялись.
Это локальное исправление. На тестовом сервере backend/frontend bd3f996; live CHR сценарий стабильности меню пока не проверен. Следующий12.5.55 — собрать и развернуть frontend, создать собственную реальную GRE пару, проверить меню при обновлении и форму, затем удалить только собственные интерфейсы/IP и проверить baseline. Реальные VPN объекты обязательны по указанию пользователя. Telegram/Portal/multi-instance и остальные внешние ограничения остаются открытыми. Локальный коммит без push.
+2 -2
View File
@@ -1,12 +1,12 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-08: 12.5.53 развернул backend/frontendbd3f996 и подтвердил peer IP на новой реальной GRE паре: receipt/native/snapshot/restore/MTU/toggle/clear и UI/reload/320px passed, Ping3/3 в обе стороны. Own GRE/IP/connected routes удалены, выбранная конфигурация и DB controls восстановлены; backend healthy. Осталась UX-недоработка: меню действий закрывается при перерисовке, потребовалось повторное открытие. Следующий12.5.54 — стабильность меню GRE при обновлении данных; реальные VPN тестовые объекты создавать и очищать по правилу пользователя.
Актуальное продолжение 2026-10-08: 12.5.54 локально исправил закрытие меню GRE при перерисовке: стабильный компонент ячейки получает актуальные обработчики и права через React context. Playwright 3/3, scoped TypeScript и ESLint passed; новый тест воспроизводит сбой на прежнем коде. Исправление ещё не развёрнуто: удалённый backend/frontend остаётся bd3f996. Следующий12.5.55 — Docker build/deploy frontend и повторная проверка меню на собственной реальной GRE паре с очисткой. Этап12.5 остаётся открытым.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-08. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
## Актуальная точка продолжения — 2026-10-08
Этапы0–11 завершены в описанных локальных границах. Этап12 открыт. На тестовом сервере161.104.46.222 работают backend/frontendbd3f996 и два CHR. Внешние проверки подтвердили PG backup/restore/rollback, остановку фоновой работы для одного экземпляра, API JWT/RBAC и браузерные состояния восстановления. Native TCP/UDP speed, история и UI проверены в границах журнала; доступ ко второму CHR восстановлен пользователем. Очередь Telegram проверена на реальном PG: dedupe, конкурентный claim, retry/version, ошибки сохранения и неопределённый исход; ответы отправителя подменялись, реальной доставки это не доказывает. Блоки12.5.43–45 подтвердили browser retry/checkpoint и развернули понятное пояснение HTTP500. 12.5.46 подтвердил browser JWT/RBAC очереди на реальном HTTP/PG; 12.5.47 подтвердил изоляцию checkpoint при смене двух реальных HTTP/PG источников в конфигурации local; 12.5.48 подтвердил scoped axe/клавиатуру в обеих темах и320px; 12.5.49 собрал Q-матрицу и подтвердил отсутствие настройки Telegram; 12.5.50 подтвердил GET-only инвентаризацию: VPN/BGP/OSPF пусты, роли home-router/home-router; По уточнению пользователя12.5.51 создал реальные GRE туннели и подтвердил lifecycle/связность; обнаружена потеря peer IP, 12.5.52 локально исправил сохранение peer metadata, профильные проверки passed; 12.5.53 развернул исправление и подтвердил его на реальной GRE паре/API/UI; следующий12.5.54 — стабильность меню действий GRE при перерисовке. Реальная Telegram доставка, Portal SSO, физический TCP disconnect, multi-instance и остальные профильные сетевые сценарии остаются открытыми. [Протокол](ux-ui-external-acceptance.md) и [журнал](ux-ui-stage-12.md) содержат доказательства и ограничения.
Этапы0–11 завершены в описанных локальных границах. Этап12 открыт. На тестовом сервере161.104.46.222 работают backend/frontendbd3f996 и два CHR. Внешние проверки подтвердили PG backup/restore/rollback, остановку фоновой работы для одного экземпляра, API JWT/RBAC и браузерные состояния восстановления. Native TCP/UDP speed, история и UI проверены в границах журнала; доступ ко второму CHR восстановлен пользователем. Очередь Telegram проверена на реальном PG: dedupe, конкурентный claim, retry/version, ошибки сохранения и неопределённый исход; ответы отправителя подменялись, реальной доставки это не доказывает. Блоки12.5.43–45 подтвердили browser retry/checkpoint и развернули понятное пояснение HTTP500. 12.5.46 подтвердил browser JWT/RBAC очереди на реальном HTTP/PG; 12.5.47 подтвердил изоляцию checkpoint при смене двух реальных HTTP/PG источников в конфигурации local; 12.5.48 подтвердил scoped axe/клавиатуру в обеих темах и320px; 12.5.49 собрал Q-матрицу и подтвердил отсутствие настройки Telegram; 12.5.50 подтвердил GET-only инвентаризацию: VPN/BGP/OSPF пусты, роли home-router/home-router; По уточнению пользователя12.5.51 создал реальные GRE туннели и подтвердил lifecycle/связность; обнаружена потеря peer IP, 12.5.52 локально исправил сохранение peer metadata, профильные проверки passed; 12.5.53 развернул исправление и подтвердил его на реальной GRE паре/API/UI; 12.5.54 локально исправил устойчивость меню GRE, профильные проверки passed; следующий12.5.55 — frontend Docker deploy и повторная проверка на реальной GRE паре. Реальная Telegram доставка, Portal SSO, физический TCP disconnect, multi-instance и остальные профильные сетевые сценарии остаются открытыми. [Протокол](ux-ui-external-acceptance.md) и [журнал](ux-ui-stage-12.md) содержат доказательства и ограничения.
Выполненные внешние сценарии не заменяют приёмку оставшихся интеграций и условий эксплуатации. Датированные продолжения ниже — история; точный следующий шаг указан в начале [журнала](ux-ui-progress.md).
+10 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-08: 12.5.53 развернул backend/frontendbd3f996 и подтвердил peer IP на новой реальной GRE паре: receipt/native/snapshot/restore/MTU/toggle/clear и UI/reload/320px passed, Ping3/3 в обе стороны. Own GRE/IP/connected routes удалены, выбранная конфигурация и DB controls восстановлены; backend healthy. Осталась UX-недоработка: меню действий закрывается при перерисовке, потребовалось повторное открытие. Следующий12.5.54 — стабильность меню GRE при обновлении данных; реальные VPN тестовые объекты создавать и очищать по правилу пользователя.
Актуальное продолжение 2026-10-08: 12.5.54 локально исправил закрытие меню GRE при перерисовке: стабильный компонент ячейки получает актуальные обработчики и права через React context. Playwright 3/3, scoped TypeScript и ESLint passed; новый тест воспроизводит сбой на прежнем коде. Исправление ещё не развёрнуто: удалённый backend/frontend остаётся bd3f996. Следующий12.5.55 — Docker build/deploy frontend и повторная проверка меню на собственной реальной GRE паре с очисткой. Этап12.5 остаётся открытым.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -34,7 +34,15 @@
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–53 и ограничения ниже. Runtime backend/frontendbd3f996. |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–54 и ограничения ниже. Runtime backend/frontendbd3f996. |
## 12.5.54 — устойчивость меню GRE при обновлении таблицы (2026-10-08)
При обновлении callbacks/прав пересоздавался inline renderer actions column; React монтировал ячейку заново и закрывал Base UI меню. В components/data-grids/gre-tunnels-data-grid.tsx вынесен стабильный GreActionsCell, текущие callbacks/permissions передаются через типизированный React context. ReUI composition, оформление, API таблицы и ключ строки serverId/id сохранены. Политика блокировки операций при загрузке и отсутствии доступа сохранена.
Проверки: новый tests/e2e/gre-menu-refresh.spec.ts открывает меню, запускает GET без outside pointer event, проверяет сохранение меню, disabled во время загрузки, восстановление enabled и свежий peer IP в форме. На прежнем HEAD тест падает именно из-за исчезнувшего menuitem; исправленный код проходит. Вместе с gre-toggle.spec.ts — 3/3 passed. Scoped TypeScript (tmp/tsconfig-speed-check.json) и ESLint изменённых файлов passed. Первую фикстуру скорректировали: инициализация источника допускает несколько GET, задержка включается только при проверяемом refresh. ReUI data-grid checklist проверен, общие компоненты не менялись.
Это локальное исправление. На тестовом сервере backend/frontend bd3f996; live CHR сценарий стабильности меню пока не проверен. Следующий12.5.55 — собрать и развернуть frontend, создать собственную реальную GRE пару, проверить меню при обновлении и форму, затем удалить только собственные интерфейсы/IP и проверить baseline. Реальные VPN объекты обязательны по указанию пользователя. Telegram/Portal/multi-instance и остальные внешние ограничения остаются открытыми. Локальный коммит без push.
## 12.5.53 — развёртывание peer metadata и реальный GRE regression (2026-10-08)
+9 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI
Актуальное продолжение 2026-10-08: 12.5.53 развернул backend/frontendbd3f996 и подтвердил peer IP на новой реальной GRE паре: receipt/native/snapshot/restore/MTU/toggle/clear и UI/reload/320px passed, Ping3/3 в обе стороны. Own GRE/IP/connected routes удалены, выбранная конфигурация и DB controls восстановлены; backend healthy. Осталась UX-недоработка: меню действий закрывается при перерисовке, потребовалось повторное открытие. Следующий12.5.54 — стабильность меню GRE при обновлении данных; реальные VPN тестовые объекты создавать и очищать по правилу пользователя.
Актуальное продолжение 2026-10-08: 12.5.54 локально исправил закрытие меню GRE при перерисовке: стабильный компонент ячейки получает актуальные обработчики и права через React context. Playwright 3/3, scoped TypeScript и ESLint passed; новый тест воспроизводит сбой на прежнем коде. Исправление ещё не развёрнуто: удалённый backend/frontend остаётся bd3f996. Следующий12.5.55 — Docker build/deploy frontend и повторная проверка меню на собственной реальной GRE паре с очисткой. Этап12.5 остаётся открытым.
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
@@ -1542,3 +1542,11 @@ gre-comment.test.ts проверил create plan→native comment→map→snapsh
Первые browser попытки остановились на меню, которое исчезало при перерисовке. Таймер первой пары удалил собственные объекты. Немедленная сверка /ip/route дала mismatch; финальный harness добавил ограниченное ожидание сходимости маршрутов, не меняя их вручную. Итоговый запуск exit0: DELETE200/200, все выбранные поля GRE/IP/routes/IPv4 Firewall совпали с baseline; CHR1 потребовал одно повторное чтение через250ms, CHR2 совпал сразу. Тестовое меню в успешном проходе пришлось открыть повторно1раз: это не исправленный UX, а отдельная выявленная недоработка для54. Проверка не является доказательством неизменности всех runtime параметров/полного export.
Own GRE интерфейсы, адреса и connected route удалены; эфемерный контейнер и туннель8018 закрыты, остались три основные Compose-службы. Main controls до/после: servers2/speedHistory9/retention14/migrations9 совпадают; GRE audit revisions намеренно сохранены. Следующий12.5.54 — сохранить устойчивость меню действий GRE при перерисовке и проверить профильный сценарий; затем продолжать настоящие VPN объекты. Права, частичные отказы остальных VPN, Portal/Telegram и прочие внешние ограничения остаются открытыми. Локальный коммит без push.
## 12.5.54 — устойчивость меню GRE при обновлении таблицы (2026-10-08)
При обновлении callbacks/прав пересоздавался inline renderer actions column; React монтировал ячейку заново и закрывал Base UI меню. В components/data-grids/gre-tunnels-data-grid.tsx вынесен стабильный GreActionsCell, текущие callbacks/permissions передаются через типизированный React context. ReUI composition, оформление, API таблицы и ключ строки serverId/id сохранены. Политика блокировки операций при загрузке и отсутствии доступа сохранена.
Проверки: новый tests/e2e/gre-menu-refresh.spec.ts открывает меню, запускает GET без outside pointer event, проверяет сохранение меню, disabled во время загрузки, восстановление enabled и свежий peer IP в форме. На прежнем HEAD тест падает именно из-за исчезнувшего menuitem; исправленный код проходит. Вместе с gre-toggle.spec.ts — 3/3 passed. Scoped TypeScript (tmp/tsconfig-speed-check.json) и ESLint изменённых файлов passed. Первую фикстуру скорректировали: инициализация источника допускает несколько GET, задержка включается только при проверяемом refresh. ReUI data-grid checklist проверен, общие компоненты не менялись.
Это локальное исправление. На тестовом сервере backend/frontend bd3f996; live CHR сценарий стабильности меню пока не проверен. Следующий12.5.55 — собрать и развернуть frontend, создать собственную реальную GRE пару, проверить меню при обновлении и форму, затем удалить только собственные интерфейсы/IP и проверить baseline. Реальные VPN объекты обязательны по указанию пользователя. Telegram/Portal/multi-instance и остальные внешние ограничения остаются открытыми. Локальный коммит без push.
+8
View File
@@ -385,3 +385,11 @@ gre-comment.test.ts проверил create plan→native comment→map→snapsh
Первые browser попытки остановились на меню, которое исчезало при перерисовке. Таймер первой пары удалил собственные объекты. Немедленная сверка /ip/route дала mismatch; финальный harness добавил ограниченное ожидание сходимости маршрутов, не меняя их вручную. Итоговый запуск exit0: DELETE200/200, все выбранные поля GRE/IP/routes/IPv4 Firewall совпали с baseline; CHR1 потребовал одно повторное чтение через250ms, CHR2 совпал сразу. Тестовое меню в успешном проходе пришлось открыть повторно1раз: это не исправленный UX, а отдельная выявленная недоработка для54. Проверка не является доказательством неизменности всех runtime параметров/полного export.
Own GRE интерфейсы, адреса и connected route удалены; эфемерный контейнер и туннель8018 закрыты, остались три основные Compose-службы. Main controls до/после: servers2/speedHistory9/retention14/migrations9 совпадают; GRE audit revisions намеренно сохранены. Следующий12.5.54 — сохранить устойчивость меню действий GRE при перерисовке и проверить профильный сценарий; затем продолжать настоящие VPN объекты. Права, частичные отказы остальных VPN, Portal/Telegram и прочие внешние ограничения остаются открытыми. Локальный коммит без push.
## 12.5.54 — устойчивость меню GRE при обновлении таблицы (2026-10-08)
При обновлении callbacks/прав пересоздавался inline renderer actions column; React монтировал ячейку заново и закрывал Base UI меню. В components/data-grids/gre-tunnels-data-grid.tsx вынесен стабильный GreActionsCell, текущие callbacks/permissions передаются через типизированный React context. ReUI composition, оформление, API таблицы и ключ строки serverId/id сохранены. Политика блокировки операций при загрузке и отсутствии доступа сохранена.
Проверки: новый tests/e2e/gre-menu-refresh.spec.ts открывает меню, запускает GET без outside pointer event, проверяет сохранение меню, disabled во время загрузки, восстановление enabled и свежий peer IP в форме. На прежнем HEAD тест падает именно из-за исчезнувшего menuitem; исправленный код проходит. Вместе с gre-toggle.spec.ts — 3/3 passed. Scoped TypeScript (tmp/tsconfig-speed-check.json) и ESLint изменённых файлов passed. Первую фикстуру скорректировали: инициализация источника допускает несколько GET, задержка включается только при проверяемом refresh. ReUI data-grid checklist проверен, общие компоненты не менялись.
Это локальное исправление. На тестовом сервере backend/frontend bd3f996; live CHR сценарий стабильности меню пока не проверен. Следующий12.5.55 — собрать и развернуть frontend, создать собственную реальную GRE пару, проверить меню при обновлении и форму, затем удалить только собственные интерфейсы/IP и проверить baseline. Реальные VPN объекты обязательны по указанию пользователя. Telegram/Portal/multi-instance и остальные внешние ограничения остаются открытыми. Локальный коммит без push.
+41
View File
@@ -0,0 +1,41 @@
import { test, expect } from "@playwright/test"
test("GRE: открытое меню переживает обновление и использует свежие права и данные", async ({ page }) => {
let loads = 0
let refreshing = false
let release: (() => void) | undefined
const gate = new Promise<void>(resolve => { release = resolve })
const tunnel = { id: "*M", name: "gre-menu", serverId: "1", localAddress: "192.0.2.1", remoteAddress: "192.0.2.2", localInnerIp: "10.0.0.1/30", remoteInnerIp: "10.0.0.2/30", poolId: "pool", ipsec: null, mtu: 1400, keepaliveInterval: 10, keepaliveRetries: 3, dscp: "inherit", clampTcpMss: true, allowFastPath: true, comment: "", enabled: true, status: "up" }
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
await page.route("**/api/**", async route => {
const path = new URL(route.request().url()).pathname
if (path === "/api/gre/tunnels") {
loads++
if (refreshing) await gate
return route.fulfill({ json: { tunnels: [{ ...tunnel, remoteInnerIp: refreshing ? "10.0.0.6/30" : tunnel.remoteInnerIp }] } })
}
return route.fulfill({ json: path === "/api/auth/config" ? { required: false }
: path === "/api/servers" ? [{ id: 1, name: "Роутер", host: "192.0.2.1", enabled: true, type: "exit-node", country: "RU", status: "online" }] : {} })
})
await page.goto("/gre")
const refresh = page.getByRole("button", { name: "Обновить", exact: true })
await expect(refresh).toBeEnabled()
await page.getByRole("button", { name: "Действия: gre-menu" }).click()
const edit = page.getByRole("menuitem", { name: /Редактировать/ })
await expect(edit).toBeEnabled()
// Invoke refresh without an outside pointer event: that event should close a menu.
const initialLoads = loads
refreshing = true
await refresh.evaluate(element => (element as HTMLButtonElement).click())
await expect.poll(() => loads).toBe(initialLoads + 1)
await expect(edit).toBeVisible()
await expect(edit).toBeDisabled()
release!()
await expect(refresh).toBeEnabled()
await expect(edit).toBeVisible()
await expect(edit).toBeEnabled()
await edit.click()
const dialog = page.getByRole("dialog", { name: "Редактировать GRE-туннель" })
await expect(dialog).toBeVisible()
await expect(dialog.getByLabel("Удалённый IP", { exact: true })).toHaveValue("10.0.0.6/30")
})