fix(ui): исправить фокус списка серверов и контраст общих элементов
This commit is contained in:
1 parent
9b0d7bb6e8
commit
3c9096804b
21 files changed
+6952
-58
No files matched your search
@@ -761,11 +761,11 @@ function BgpPageContent({ selectedServerId, selectedSessionId, activeTab, onSele
|
||||
}
|
||||
banner={
|
||||
<div className="border-b bg-background shrink-0">
|
||||
<div className="flex items-center px-6">
|
||||
<div className="flex min-w-0 items-center overflow-x-auto px-4 sm:px-6">
|
||||
{TABS.map(t => (
|
||||
<button key={t.id} aria-pressed={activeTab === t.id} onClick={() => onSelectTab(t.id)}
|
||||
<button key={t.id} type="button" aria-pressed={activeTab === t.id} onClick={() => onSelectTab(t.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px",
|
||||
"flex shrink-0 items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px",
|
||||
activeTab === t.id
|
||||
? "border-primary text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border",
|
||||
|
||||
@@ -334,7 +334,7 @@ function VxlanPageContent({ selectedServerId, selectedTunnelId, allTunnelsHref,
|
||||
<div className="flex items-start gap-3 rounded-lg bg-sky-500/5 border border-sky-500/20 px-4 py-3 text-sm">
|
||||
<NetworkIcon className="size-5 text-sky-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-sky-600 dark:text-sky-400">VXLAN — L2-over-L3 оверлей для RouterOS 7.x</p>
|
||||
<p className="font-medium text-foreground">VXLAN — L2-over-L3 оверлей для RouterOS 7.x</p>
|
||||
<p className="text-muted-foreground text-xs mt-0.5">
|
||||
VXLAN переносит L2 через IP-сеть. VNI определяет сегмент; шифрование зависит от настроенного транспорта.
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import fs from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { chromium, expect } from '@playwright/test';
|
||||
import axe from 'axe-core';
|
||||
|
||||
// Deliberately read-only. The fixture stays outside Git; no response fixtures are used.
|
||||
const credentials = JSON.parse(fs.readFileSync(process.env.MMAPP_PORTAL_FIXTURE || 'tmp/portal195-private.json'));
|
||||
const output = process.env.MMAPP_UI_AUDIT_OUTPUT || 'tmp/ui260-audit.json';
|
||||
assert(!fs.existsSync(output), 'Never overwrite an acceptance report');
|
||||
const origin = 'http://localhost:3000';
|
||||
const routes = (process.env.MMAPP_UI_AUDIT_ROUTES || '/dashboard,/servers,/uptime,/traffic,/statistics,/wireguard,/ipsec,/gre,/vxlan,/ospf,/bgp,/filters,/backups,/certificates,/containers,/alerts,/settings,/releases').split(',');
|
||||
assert(routes.every(path => /^\/[a-z-]+$/.test(path)));
|
||||
const requiredReads = {
|
||||
'/dashboard': ['/api/servers', '/api/uptime/overview', '/api/filters/rules', '/api/bgp/catalog', '/api/internet-path/latest', '/api/filters/gre-tunnels', '/api/wireguard', '/api/traffic/servers', '/api/vxlan'],
|
||||
'/servers': ['/api/servers'], '/uptime': ['/api/uptime/overview'], '/traffic': ['/api/traffic/servers'],
|
||||
'/statistics': ['/api/statistics'], '/wireguard': ['/api/wireguard'], '/ipsec': ['/api/ipsec'],
|
||||
'/gre': ['/api/gre/tunnels'], '/vxlan': ['/api/vxlan'], '/ospf': ['/api/ospf/catalog'],
|
||||
'/bgp': ['/api/bgp/catalog'], '/filters': ['/api/filters/rules'], '/backups': ['/api/backups'],
|
||||
'/certificates': ['/api/certificates/state'], '/containers': ['/api/containers/state'], '/alerts': ['/api/alerts'],
|
||||
'/users': ['/api/users'], '/firewall': ['/api/firewall/all'], '/probes': ['/api/servers'], '/terminal': ['/api/servers'],
|
||||
'/data-collection': ['/api/scheduler/status'], '/recursive-routes': ['/api/servers'],
|
||||
'/route-optimizer': ['/api/servers', '/api/filters/rules', '/api/uptime/speed-probes'],
|
||||
'/network-map': ['/api/servers', '/api/filters/gre-tunnels', '/api/uptime/speed-probes'],
|
||||
'/ip-ranges': ['/api/evobgp/settings'], '/asns': ['/api/evobgp/settings'], '/communities': ['/api/evobgp/settings'], '/domains': ['/api/evobgp/settings'],
|
||||
};
|
||||
const report = {
|
||||
date: '2026-10-15', block: process.env.MMAPP_UI_AUDIT_BLOCK || '12.5.260', origin, realPortalUiLogin: true,
|
||||
responseMocks: false, deviceWritesPermitted: false, cases: [], blockedWrites: [], readOnlyPosts: [],
|
||||
bounds: ['Default rendered state only; data can still be refreshing', 'Synthetic lab timing over SSH, no CPU/network throttling; not field Core Web Vitals', 'Automated WCAG-tagged checks, not manual screen-reader certification'],
|
||||
};
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||
const tags = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'];
|
||||
try {
|
||||
for (const theme of ['dark', 'light']) {
|
||||
const context = await browser.newContext();
|
||||
await context.addInitScript(value => {
|
||||
localStorage.setItem('rl-theme', value);
|
||||
window.__acceptanceMetrics = { lcp: null, cls: 0, longTasks: 0, longTaskMs: 0 };
|
||||
for (const type of ['largest-contentful-paint', 'layout-shift', 'longtask']) {
|
||||
try {
|
||||
new PerformanceObserver(list => {
|
||||
for (const e of list.getEntries()) {
|
||||
const m = window.__acceptanceMetrics;
|
||||
if (type === 'largest-contentful-paint') m.lcp = e.startTime;
|
||||
if (type === 'layout-shift' && !e.hadRecentInput) m.cls += e.value;
|
||||
if (type === 'longtask') { m.longTasks++; m.longTaskMs += e.duration; }
|
||||
}
|
||||
}).observe({ type, buffered: true });
|
||||
} catch { /* Browser may not expose every observer. */ }
|
||||
}
|
||||
}, theme);
|
||||
const page = await context.newPage();
|
||||
await page.goto(origin + '/dashboard');
|
||||
await page.getByLabel('Email', { exact: true }).fill(credentials.email);
|
||||
await page.getByLabel('Пароль', { exact: true }).fill(credentials.password);
|
||||
await page.getByRole('button', { name: 'Войти', exact: true }).click();
|
||||
await expect(page.getByRole('navigation', { name: 'Путь страницы' })).toBeVisible({ timeout: 30000 });
|
||||
await page.route('**/api/**', async route => {
|
||||
const r = route.request();
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(r.method())) {
|
||||
const path = new URL(r.url()).pathname;
|
||||
// These POST handlers were inspected: DNS resolution, remote GET catalog,
|
||||
// and a computed OSPF preview. None applies device configuration.
|
||||
const readQuery = r.method() === 'POST' && (path === '/api/network/resolve-hosts' || path === '/api/evobgp/catalog' || /^\/api\/servers\/\d+\/ospf\/optimize\/preview$/.test(path));
|
||||
if (process.env.MMAPP_UI_AUDIT_ALLOW_READ_POSTS === 'true' && readQuery) {
|
||||
report.readOnlyPosts.push({ theme, path });
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
report.blockedWrites.push({ theme, method: r.method(), path: new URL(r.url()).pathname });
|
||||
await route.abort('blockedbyclient');
|
||||
} else await route.continue();
|
||||
});
|
||||
for (const path of routes) for (const width of [1440, 320]) {
|
||||
const item = { theme, path, width, pageErrors: [], apiErrors: [] };
|
||||
const successfulReads = new Set();
|
||||
const onError = error => item.pageErrors.push(error.name);
|
||||
const onResponse = response => {
|
||||
const u = new URL(response.url());
|
||||
if (u.origin === origin && u.pathname.startsWith('/api/') && response.status() >= 400) item.apiErrors.push({ path: u.pathname, status: response.status() });
|
||||
if (u.origin === origin && response.status() === 200 && response.request().method() === 'GET') successfulReads.add(u.pathname);
|
||||
};
|
||||
page.on('pageerror', onError); page.on('response', onResponse);
|
||||
try {
|
||||
await page.setViewportSize({ width, height: 1000 });
|
||||
const start = Date.now();
|
||||
await page.goto(origin + path, { waitUntil: 'domcontentloaded', timeout: 35000 });
|
||||
await expect(page.locator('#main-content')).toBeVisible({ timeout: 30000 });
|
||||
await expect(page.getByRole('navigation', { name: 'Путь страницы' })).toBeVisible();
|
||||
item.mainVisibleMs = Date.now() - start;
|
||||
if (requiredReads[path]) {
|
||||
await expect.poll(() => requiredReads[path].every(endpoint => successfulReads.has(endpoint)), { timeout: 35000 }).toBe(true);
|
||||
item.requiredReads = requiredReads[path];
|
||||
item.dataResponsesMs = Date.now() - start;
|
||||
}
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
await new Promise(resolve => setTimeout(resolve, 1200));
|
||||
await Promise.race([
|
||||
Promise.allSettled(document.getAnimations().filter(a => a.effect?.getComputedTiming().iterations !== Infinity).map(a => a.finished)),
|
||||
new Promise(resolve => setTimeout(resolve, 1500)),
|
||||
]);
|
||||
});
|
||||
item.layout = await page.evaluate(() => ({
|
||||
overflow: document.documentElement.scrollWidth > innerWidth + 1,
|
||||
mainOverflow: document.querySelector('#main-content').scrollWidth > document.querySelector('#main-content').clientWidth + 1,
|
||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
||||
pendingSkeletons: document.querySelectorAll('#main-content [data-slot="skeleton"]').length,
|
||||
}));
|
||||
assert.equal(item.layout.theme, theme);
|
||||
item.performance = await page.evaluate(() => {
|
||||
const n = performance.getEntriesByType('navigation')[0];
|
||||
return { ...window.__acceptanceMetrics, ttfbMs: n.responseStart - n.requestStart, domInteractiveMs: n.domInteractive, durationMs: n.duration };
|
||||
});
|
||||
await page.addScriptTag({ content: axe.source });
|
||||
item.violations = await page.evaluate(async values => {
|
||||
const result = await window.axe.run({ include: ['#main-content', '[aria-label="Основная навигация"]'] }, { runOnly: { type: 'tag', values } });
|
||||
return result.violations.map(v => ({ id: v.id, impact: v.impact, targets: v.nodes.map(n => n.target) }));
|
||||
}, tags);
|
||||
item.auditCompleted = true;
|
||||
} catch (error) { item.failure = { name: error.name, message: error.message.slice(0, 250) }; }
|
||||
finally {
|
||||
page.off('pageerror', onError); page.off('response', onResponse);
|
||||
report.cases.push(item);
|
||||
fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
|
||||
console.log(JSON.stringify({ theme, path, width, violations: item.violations?.map(v => v.id), overflow: item.layout?.overflow, failure: item.failure?.name }));
|
||||
}
|
||||
}
|
||||
await context.close();
|
||||
}
|
||||
report.completed = true;
|
||||
report.passed = report.blockedWrites.length === 0 && report.cases.every(c => c.auditCompleted && !c.layout.overflow && !c.layout.mainOverflow && c.violations.length === 0 && c.pageErrors.length === 0 && c.apiErrors.length === 0);
|
||||
if (!report.passed) process.exitCode = 1;
|
||||
} finally {
|
||||
fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
|
||||
await browser.close();
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@tanstack/react-table"
|
||||
import type { Server, ServerType } from "@/lib/data"
|
||||
import { StatusBadge } from "@/components/status-badge"
|
||||
import { Badge, type BadgeProps } from "@/components/reui/badge"
|
||||
import { Flag } from "@/components/flag"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -49,10 +50,10 @@ const TYPE_LABELS: Record<ServerType, string> = {
|
||||
"home-router": "Home Router",
|
||||
}
|
||||
|
||||
const TYPE_STYLES: Record<ServerType, string> = {
|
||||
"jump-host": "bg-violet-500/10 text-violet-400 border-violet-500/20",
|
||||
"exit-node": "bg-sky-500/10 text-sky-400 border-sky-500/20",
|
||||
"home-router": "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
|
||||
const TYPE_VARIANTS: Record<ServerType, BadgeProps["variant"]> = {
|
||||
"jump-host": "primary-light",
|
||||
"exit-node": "info-light",
|
||||
"home-router": "success-light",
|
||||
}
|
||||
|
||||
function rosVer(os: string): number {
|
||||
@@ -63,13 +64,13 @@ function rosVer(os: string): number {
|
||||
|
||||
function TypeBadge({ type }: { type: ServerType }) {
|
||||
const icon =
|
||||
type === "jump-host" ? <ServerIcon className="size-3 mr-1" />
|
||||
: type === "exit-node" ? <NetworkIcon className="size-3 mr-1" />
|
||||
: <HomeIcon className="size-3 mr-1" />
|
||||
type === "jump-host" ? <ServerIcon aria-hidden="true" />
|
||||
: type === "exit-node" ? <NetworkIcon aria-hidden="true" />
|
||||
: <HomeIcon aria-hidden="true" />
|
||||
return (
|
||||
<span className={cn("inline-flex items-center text-xs font-medium border rounded px-2 py-0.5", TYPE_STYLES[type])}>
|
||||
<Badge variant={TYPE_VARIANTS[type]} className="text-foreground">
|
||||
{icon}{TYPE_LABELS[type]}
|
||||
</span>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ function SegmentedControl<T extends string>({
|
||||
>
|
||||
{option.label}
|
||||
{option.count != null && (
|
||||
<span className="text-xs tabular-nums opacity-60">{option.count}</span>
|
||||
<span className="text-xs tabular-nums">{option.count}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -35,6 +35,10 @@ import { LayersIcon, SearchIcon, ServerIcon } from "lucide-react"
|
||||
/** Preview: https://reui.io/preview/base/list-9 · https://reui.io/docs/components/base/icon-tile · https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/badge */
|
||||
export const ALL_SERVERS_ID = "all"
|
||||
|
||||
// Device URL changes can remount the page. Keep only the current keyboard intent,
|
||||
// and never restore it after the user has focused another control.
|
||||
let pendingKeyboardFocus: { path: string; id: string; source: Element | null; until: number } | null = null
|
||||
|
||||
export interface ServerTileItem {
|
||||
id: string
|
||||
name: string
|
||||
@@ -185,7 +189,15 @@ function ServerTileRail({
|
||||
useEffect(() => {
|
||||
const viewport = listRef.current?.closest<HTMLElement>('[data-slot="scroll-area-viewport"]')
|
||||
viewport?.scrollTo({ top: 0 })
|
||||
listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" })
|
||||
const selected = listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')
|
||||
selected?.scrollIntoView({ block: "nearest" })
|
||||
const pending = pendingKeyboardFocus
|
||||
if (pending && (pending.until < performance.now() || pending.path !== window.location.pathname)) {
|
||||
pendingKeyboardFocus = null
|
||||
} else if (pending && pending.id === resolvedId && selected?.getClientRects().length) {
|
||||
if (document.activeElement === document.body || document.activeElement === pending.source) selected.focus({ preventScroll: true })
|
||||
pendingKeyboardFocus = null
|
||||
}
|
||||
}, [pageIndex, resolvedId, q, pageSize])
|
||||
const visibleIds = new Set(table.getRowModel().rows.map(row => row.id))
|
||||
const visibleGroups = groups.map(group => ({ ...group, items: group.items.filter(item => visibleIds.has(item.id)) })).filter(group => group.items.length > 0)
|
||||
@@ -200,6 +212,19 @@ function ServerTileRail({
|
||||
return ids
|
||||
}, [filtered, showAllTile])
|
||||
|
||||
const tabStopId = activeId && selectableIds.includes(activeId)
|
||||
? activeId
|
||||
: showAllTile ? ALL_SERVERS_ID : table.getRowModel().rows.find(row => isSelectable(row.original))?.id
|
||||
|
||||
function selectWithKeyboard(id: string) {
|
||||
if (id === resolvedId) {
|
||||
listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')?.focus()
|
||||
return
|
||||
}
|
||||
pendingKeyboardFocus = { path: window.location.pathname, id, source: document.activeElement, until: performance.now() + 10000 }
|
||||
onSelect(id)
|
||||
}
|
||||
|
||||
function moveSelection(delta: number) {
|
||||
if (selectableIds.length === 0) return
|
||||
const idx = selectableIds.indexOf(resolvedId)
|
||||
@@ -208,7 +233,7 @@ function ServerTileRail({
|
||||
? delta > 0 ? 0 : selectableIds.length - 1
|
||||
: Math.min(selectableIds.length - 1, Math.max(0, idx + delta))
|
||||
const nextId = selectableIds[nextIdx]
|
||||
if (nextId) onSelect(nextId)
|
||||
if (nextId) selectWithKeyboard(nextId)
|
||||
}
|
||||
|
||||
function handleListKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
||||
@@ -225,13 +250,13 @@ function ServerTileRail({
|
||||
}
|
||||
if (event.key === "Home") {
|
||||
event.preventDefault()
|
||||
if (selectableIds[0]) onSelect(selectableIds[0])
|
||||
if (selectableIds[0]) selectWithKeyboard(selectableIds[0])
|
||||
return
|
||||
}
|
||||
if (event.key === "End") {
|
||||
event.preventDefault()
|
||||
const last = selectableIds[selectableIds.length - 1]
|
||||
if (last) onSelect(last)
|
||||
if (last) selectWithKeyboard(last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,8 +308,6 @@ function ServerTileRail({
|
||||
className="flex flex-col gap-0.5 p-1.5"
|
||||
role="listbox"
|
||||
aria-label="Серверы"
|
||||
aria-activedescendant={activeId ? `${railId}-server-tile-${activeId}` : undefined}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleListKeyDown}
|
||||
>
|
||||
{showAllTile ? (
|
||||
@@ -292,6 +315,7 @@ function ServerTileRail({
|
||||
id={`${railId}-server-tile-${ALL_SERVERS_ID}`}
|
||||
item={allItem}
|
||||
selected={resolvedId === ALL_SERVERS_ID}
|
||||
tabStop={tabStopId === ALL_SERVERS_ID}
|
||||
onSelect={() => onSelect(ALL_SERVERS_ID)}
|
||||
showCount={showCount}
|
||||
showType={false}
|
||||
@@ -315,6 +339,7 @@ function ServerTileRail({
|
||||
id={`${railId}-server-tile-${item.id}`}
|
||||
item={item}
|
||||
selected={resolvedId === item.id}
|
||||
tabStop={tabStopId === item.id}
|
||||
onSelect={() => onSelect(item.id)}
|
||||
showCount={showCount}
|
||||
showType={showType}
|
||||
@@ -354,6 +379,7 @@ function ServerTileButton({
|
||||
id,
|
||||
item,
|
||||
selected,
|
||||
tabStop,
|
||||
onSelect,
|
||||
showCount,
|
||||
showType,
|
||||
@@ -363,6 +389,7 @@ function ServerTileButton({
|
||||
id: string
|
||||
item: ServerTileItem
|
||||
selected: boolean
|
||||
tabStop: boolean
|
||||
onSelect: () => void
|
||||
showCount: boolean
|
||||
showType: boolean
|
||||
@@ -385,6 +412,7 @@ function ServerTileButton({
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
tabIndex={tabStop ? 0 : -1}
|
||||
title={item.title ?? hostname}
|
||||
disabled={!selectable}
|
||||
onClick={onSelect}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# UX/UI: протокол внешней приёмки 12.5
|
||||
|
||||
Актуальное продолжение 2026-10-15: 259 — реальный first issue и scheduler renewal приняты: trusted TLS сменил SHA-256, один logical leaf, старые native ID/fingerprint/private-key сохранены в архиве. Native/export/SSL/SQL/DNS восстановлены, private fixtures удалены. Runtime backend258/frontend251 healthy. Следующий260 — актуализировать остаток и выполнить профильную приёмку UX/доступности/производительности; этап12.5 открыт.
|
||||
Актуальное продолжение 2026-10-15: 260 — исправлены общие focus/target-size/contrast и BGP320 tabs; production frontend ux-accessibility260-final развёрнут, backend258 healthy.72 live WCAG/layout состояния прошли,59 уникальных E2E прошли в записанных итерациях; synthetic timing сохранён. Следующий261 — оставшиеся12 экранов и точные границы приёмки; этап12.5 открыт.
|
||||
|
||||
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
|
||||
|
||||
@@ -18,6 +18,27 @@ OSPF84: real GRE Full adjacency/private routes/Ping3/3 и4 live PC/320px URL/bro
|
||||
|
||||
BFD85: Up/Full/own route/Ping,4 live UI и loss/withdrawal/recovery приняты; exact own cleanup8 коллекций×2 passed. Withdrawal5,245ms не подтверждает sub-second BFD timing; этот вопрос открыт. Runtime routing83 без новой сборки. [Отчёт](ux-ui/acceptance/ospf-bfd-loss-2026-10-09.json).
|
||||
|
||||
## 12.5.260 — общая доступность, клавиатура и профильная скорость UI (2026-10-15)
|
||||
|
||||
Проведён read-only аудит18 разделов×2 темы×1440/320px:72 состояния текущего стенда/настоящий Portal UI, без response mocks. Baseline выявил23 состояния с axe нарушениями: недостаточная независимая pointer-area у focusable listbox-контейнера, контраст счётчиков SegmentedControl, raw type badges серверов и заголовка VXLAN. Document overflow0/JS errors0/API errors0; BGP320 имел2 внутренних переполнения main. Dashboard в baseline ещё содержал skeleton: его ранние timings не считаются готовностью данных. [Baseline](ux-ui/acceptance/ui260-baseline.json).
|
||||
|
||||
ServerTileRail сохраняет существующие Item/ScrollArea/DataGrid/44px варианты: один Tab stop на option, стрелки/Home/End перемещают selection и DOM focus, включая переход через страницу50. Root больше не конкурирует с вложенными option за фокус/target. Первые2 новые keyboard tests обнаружили remount GrePageContent при смене server URL; короткий10s keyboard intent восстанавливает фокус на новом выбранном элементе, только если пользователь не сфокусировал другой control, и игнорирует скрытый rail. Настройки устройств/URL selection/API не изменены.
|
||||
|
||||
TypeBadge переиспользует установленный ReUI Badge primary/info/success-light и text-foreground (как существующий rail), поскольку текущий dark success-foreground не прошёл4.5:1; vendor/style/theme не менялись, декоративные иконки скрыты от AT. У счётчиков снята opacity, заголовок VXLAN использует text-foreground. Вкладки BGP на узком экране имеют собственную горизонтальную прокрутку и не сжимаются. [Badge API](https://reui.io/docs/components/base/badge?ref=mcp), [preview](https://reui.io/components/badge?ref=mcp); audit checklist получен через MCP, выполнен в изменённых границах.
|
||||
|
||||
Локально initial57 E2E:55 passed/2 keyboard failed; после исправления remount rerun6 (2 keyboard+4 GRE/VXLAN) passed. Затем добавлены2 contrast tests всех3 типов серверов; final shared4 (2 contrast+2 keyboard) passed. Это59 уникальных принятых случаев; повторные прогоны не прибавляются к числу сценариев; initial отказ сохранён в сводке. Проверены серверные операции/отказы/RBAC/формы, BGP data states, GRE/VXLAN layout и backup layout. TypeScript/scoped ESLint passed. Production frontend build passed,528 source files matched; финальный образ ux-accessibility260-final развёрнут, backend258/DB/Portal сохранены.
|
||||
|
||||
Первый live recheck72 оставил2 color-contrast failures только на servers/dark; прочие70 прошли. После text-foreground final server recheck4 прошёл. Итоговое покрытие72 уникальных состояний объединено с явным происхождением результатов, а не выдано за76 разных случаев: axe WCAG2/2.1/2.2 tagged violations0, document/main overflow0, JS/API errors0, blocked writes0. Для data pages сначала ожидаются200 required endpoint responses; dashboard после этого skeleton0. Synthetic main-visible p50/p95=548/1052ms; required API response p50/p95=1033/2494ms. Это один Edge/headless lab run через SSH, без throttling, стандартный viewport snapshot; не field CWV/maximum load/полная ручная AT сертификация. [Полный протокол](ux-ui/acceptance/ui260-final.json), [сводка](ux-ui/acceptance/ui260-summary.json).
|
||||
|
||||
Визуально просмотрены screenshots servers1440 и BGP320. На BGP320 подписи KPI ещё сокращаются; эту отдельную проблему читаемости включить в261 вместе с оставшимися экранами, не считать её проверенной axe.
|
||||
|
||||
| Референс | Схема и экран |
|
||||
| --- | --- |
|
||||
| [ReUI Badge](https://reui.io/components/badge?ref=mcp) | Тип устройства → semantic variant → читаемая подпись; [servers1440](ux-ui/screens/ui260-servers.png) |
|
||||
| [ReUI Frame](https://reui.io/components/frame?ref=mcp) | Узкий экран → прокрутка вкладок → выбранный раздел; [BGP320](ux-ui/screens/ui260-bgp.png) |
|
||||
|
||||
Устройства не перенастраивались; новых native объектов/DNS/ACME не создавали. Текущая матрица исправлена: containers248 и ACME issue250/renewal259 приняты, старое отсутствие пакета/токена больше не записано как blocker. Семь групп описывают охват, не обязательные семь новых этапов. Следующий261 —12 оставшихся default screens: users/firewall/probes/terminal/data-collection/recursive-routes/route-optimizer/network-map/ip-ranges/asns/communities/domains; затем актуальные границы выпуска. Новая реализация CA revoke/MaxMind или внешняя публикация не объявляются выполненными.
|
||||
|
||||
## 12.5.259 — реальная ротация сертификата и подтверждённый откат (2026-10-15)
|
||||
|
||||
По сохранённому разрешению254 выполнен тот же exact mmt253-8e62a941.pngs.top на104.165.244.7. Перед началом снят свежий приватный native/export/SSL/files/SQL baseline; own certificate/files/jobs/DNS0, Cloudflare token/zone/account проверены. Renewal временно выключен. Подготовленный backend258 активирован; frontend251/Portal/DB сохранены, backend healthy. [Развёртывание](ux-ui/acceptance/acme259-deployment.json).
|
||||
|
||||
+35
-14
@@ -1,8 +1,29 @@
|
||||
# План реализации UX/UI по пятичасовым окнам
|
||||
|
||||
Актуальное продолжение 2026-10-15: 259 — реальный first issue и scheduler renewal приняты: trusted TLS сменил SHA-256, один logical leaf, старые native ID/fingerprint/private-key сохранены в архиве. Native/export/SSL/SQL/DNS восстановлены, private fixtures удалены. Runtime backend258/frontend251 healthy. Следующий260 — актуализировать остаток и выполнить профильную приёмку UX/доступности/производительности; этап12.5 открыт.
|
||||
Актуальное продолжение 2026-10-15: 260 — исправлены общие focus/target-size/contrast и BGP320 tabs; production frontend ux-accessibility260-final развёрнут, backend258 healthy.72 live WCAG/layout состояния прошли,59 уникальных E2E прошли в записанных итерациях; synthetic timing сохранён. Следующий261 — оставшиеся12 экранов и точные границы приёмки; этап12.5 открыт.
|
||||
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-14. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-15. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
## 12.5.260 — общая доступность, клавиатура и профильная скорость UI (2026-10-15)
|
||||
|
||||
Проведён read-only аудит18 разделов×2 темы×1440/320px:72 состояния текущего стенда/настоящий Portal UI, без response mocks. Baseline выявил23 состояния с axe нарушениями: недостаточная независимая pointer-area у focusable listbox-контейнера, контраст счётчиков SegmentedControl, raw type badges серверов и заголовка VXLAN. Document overflow0/JS errors0/API errors0; BGP320 имел2 внутренних переполнения main. Dashboard в baseline ещё содержал skeleton: его ранние timings не считаются готовностью данных. [Baseline](ux-ui/acceptance/ui260-baseline.json).
|
||||
|
||||
ServerTileRail сохраняет существующие Item/ScrollArea/DataGrid/44px варианты: один Tab stop на option, стрелки/Home/End перемещают selection и DOM focus, включая переход через страницу50. Root больше не конкурирует с вложенными option за фокус/target. Первые2 новые keyboard tests обнаружили remount GrePageContent при смене server URL; короткий10s keyboard intent восстанавливает фокус на новом выбранном элементе, только если пользователь не сфокусировал другой control, и игнорирует скрытый rail. Настройки устройств/URL selection/API не изменены.
|
||||
|
||||
TypeBadge переиспользует установленный ReUI Badge primary/info/success-light и text-foreground (как существующий rail), поскольку текущий dark success-foreground не прошёл4.5:1; vendor/style/theme не менялись, декоративные иконки скрыты от AT. У счётчиков снята opacity, заголовок VXLAN использует text-foreground. Вкладки BGP на узком экране имеют собственную горизонтальную прокрутку и не сжимаются. [Badge API](https://reui.io/docs/components/base/badge?ref=mcp), [preview](https://reui.io/components/badge?ref=mcp); audit checklist получен через MCP, выполнен в изменённых границах.
|
||||
|
||||
Локально initial57 E2E:55 passed/2 keyboard failed; после исправления remount rerun6 (2 keyboard+4 GRE/VXLAN) passed. Затем добавлены2 contrast tests всех3 типов серверов; final shared4 (2 contrast+2 keyboard) passed. Это59 уникальных принятых случаев; повторные прогоны не прибавляются к числу сценариев; initial отказ сохранён в сводке. Проверены серверные операции/отказы/RBAC/формы, BGP data states, GRE/VXLAN layout и backup layout. TypeScript/scoped ESLint passed. Production frontend build passed,528 source files matched; финальный образ ux-accessibility260-final развёрнут, backend258/DB/Portal сохранены.
|
||||
|
||||
Первый live recheck72 оставил2 color-contrast failures только на servers/dark; прочие70 прошли. После text-foreground final server recheck4 прошёл. Итоговое покрытие72 уникальных состояний объединено с явным происхождением результатов, а не выдано за76 разных случаев: axe WCAG2/2.1/2.2 tagged violations0, document/main overflow0, JS/API errors0, blocked writes0. Для data pages сначала ожидаются200 required endpoint responses; dashboard после этого skeleton0. Synthetic main-visible p50/p95=548/1052ms; required API response p50/p95=1033/2494ms. Это один Edge/headless lab run через SSH, без throttling, стандартный viewport snapshot; не field CWV/maximum load/полная ручная AT сертификация. [Полный протокол](ux-ui/acceptance/ui260-final.json), [сводка](ux-ui/acceptance/ui260-summary.json).
|
||||
|
||||
Визуально просмотрены screenshots servers1440 и BGP320. На BGP320 подписи KPI ещё сокращаются; эту отдельную проблему читаемости включить в261 вместе с оставшимися экранами, не считать её проверенной axe.
|
||||
|
||||
| Референс | Схема и экран |
|
||||
| --- | --- |
|
||||
| [ReUI Badge](https://reui.io/components/badge?ref=mcp) | Тип устройства → semantic variant → читаемая подпись; [servers1440](ux-ui/screens/ui260-servers.png) |
|
||||
| [ReUI Frame](https://reui.io/components/frame?ref=mcp) | Узкий экран → прокрутка вкладок → выбранный раздел; [BGP320](ux-ui/screens/ui260-bgp.png) |
|
||||
|
||||
Устройства не перенастраивались; новых native объектов/DNS/ACME не создавали. Текущая матрица исправлена: containers248 и ACME issue250/renewal259 приняты, старое отсутствие пакета/токена больше не записано как blocker. Семь групп описывают охват, не обязательные семь новых этапов. Следующий261 —12 оставшихся default screens: users/firewall/probes/terminal/data-collection/recursive-routes/route-optimizer/network-map/ip-ranges/asns/communities/domains; затем актуальные границы выпуска. Новая реализация CA revoke/MaxMind или внешняя публикация не объявляются выполненными.
|
||||
|
||||
## 12.5.259 — реальная ротация сертификата и подтверждённый откат (2026-10-15)
|
||||
|
||||
@@ -314,27 +335,27 @@ Production backend/frontend ux-backups237 собраны и развёрнуты
|
||||
|
||||
Следующий238 фиксирует уже выполненное длительное REST-наблюдение. Затем — S3 restore через реальный UI в пределах собственного bridge и retention. Полная конфигурация CHR, arbitrary PKI, другие ОС, public IPv6, multi-instance и внешние jobs не приняты этим блоком.
|
||||
|
||||
## Актуальная точка продолжения — 2026-10-14
|
||||
## Актуальная точка продолжения — 2026-10-15
|
||||
|
||||
Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка201 и Portal SSO195–199 приняты в описанных границах. Multi-instance, внешние аккаунты/клиентские ОС и оставшиеся профильные сетевые ограничения открыты.
|
||||
|
||||
По поручению2026-10-09 работать укрупнёнными разделами, последовательно на текущей модели: карта возможностей → исправления → профильные тесты → build/deploy → собственные реальные сценарии → cleanup → один локальный коммит без push. Проверять квоту после завершённого раздела; незавершённые проверки явно переносить в журнал.
|
||||
|
||||
## Остаток приёмки по группам — 2026-10-14
|
||||
## Остаток приёмки по группам — 2026-10-15
|
||||
|
||||
Этапы0–11 завершены в локальных границах. Этап12.5 открыт: ниже7 групп проверок, а не7 обещанных окон или точное число тестов. Детали и исторические ограничения сохраняются в [матрице](ux-ui-remaining-acceptance.md).
|
||||
Этапы0–11 завершены в локальных границах. Этап12.5 открыт. Ниже семь групп охвата; это не семь обязательных новых этапов и не обещание числа окон. Принятые сценарии не запускать заново без изменения кода или найденного риска. Для расширения стенда отдельно нужны перечисленные условия.
|
||||
|
||||
| Группа | Что уже подтверждено | Оставшийся результат / что требуется |
|
||||
| Группа | Подтверждено | Открытый объём / условие |
|
||||
| --- | --- | --- |
|
||||
| REST и достоверность данных | Ошибки OSPF/BFD/BGP/GRE/WAN215–221 явные; latest API smoke222 прошёл | Причина единичных native401/500 неизвестна. Короткое независимое наблюдение возможно сейчас; оно не даст SLA |
|
||||
| Клиентские ОС и внешняя сеть | CHR↔CHR/Linux, private IPv4/ULA и synthetic IPv6/IPFIX; Android full-tunnel/reconnect227 подтверждены пользователем | Windows/macOS/iOS VPN, расширенная Android приёмка, независимые WAN/multi-WAN и public IPv6; public IPv6 на текущем стенде отсутствует |
|
||||
| Масштаб и длительная устойчивость | Контролируемые lock/dedupe/loss/recovery и короткие native проверки | Несколько backend/worker/writers, длительный rekey/REST/traffic, physical disconnect и maximum throughput |
|
||||
| Внешние хранилища и jobs | Локальные API/errors/RBAC/owned SQL; реальные Portal1.3.1/Telegram195–201 и Selectel S3/client/API/ownSQL228–229 | Native S3 backup/restore/live UI/retention; provider/container/Gitea/MaxMind jobs с соответствующими endpoints/аккаунтами; private/paid credentials не выдумывать |
|
||||
| Расширенные сетевые конфигурации | GRE/WG/VXLAN/IPsec/OSPF/BFD/BGP сценарии и точные границы записаны | from-pool/EUI-64/multiple-address import/export, outer IPv6, произвольная PKI/OID/policy/path-length и длительные переходы; BFD-only gate гарантией не стал |
|
||||
| Общий UX, доступность и производительность | Полная UI192 исторически; latest backend25/UI52, scoped contrast/live218 и keyboard206 | Финальная свежая UI-регрессия перед выпуском, профильный performance и ручная accessibility/screen-reader приёмка; scoped axe не полный WCAG |
|
||||
| Границы выпуска | План/доказательства/датированные блоки сохранены; коммиты локальные | Определить требуемые конфигурации и принять результат в этих границах; невыполненные варианты остаются явными ограничениями |
|
||||
| REST и достоверность данных | Нативные наблюдения236/238; ограничение shared REST pools257,12 параллельных чтений двух CHR и25 backend-профилей прошли | Длительное наблюдение после transport fix; историческая причинность всех401/500 и сравнение версий RouterOS полностью не доказаны |
|
||||
| Клиентские ОС и внешняя сеть | CHR↔CHR/Linux; Android StrongSwan full-tunnel и reconnect227 подтверждены пользователем | Windows/macOS/iOS и другие клиенты; независимые WAN/multi-WAN. Публичного IPv6 на предоставленном стенде нет — проверка требует другого подключения |
|
||||
| Масштаб и устойчивость | Контролируемые PG lock/dedupe/loss/recovery, отдельные multi-process сценарии194/200/201 | Несколько полноценных backend/worker со всеми writers, длительный rekey/traffic, физические потери WAN/DB и maximum throughput |
|
||||
| Хранилища и внешние jobs | Реальные Portal/Telegram195–201, Selectel S3/limited bridge restore/retention228–240, GeoIP mirror242; контейнеры248; production ACME250 и реальное renewal/rotation259 с trusted TLS/cleanup | Large native export/migration buckets/multi-instance S3; CA revocation отдельно не принят. Gitea publishing CI не запускался: действует no push. Direct licensed MaxMind отсутствует в текущей реализации |
|
||||
| Расширенные сетевые конфигурации | GRE/WG/VXLAN/IPsec/OSPF/BFD/BGP в записанных сценариях, включая private IPv6 | from-pool/EUI-64/multiple-address import/export, outer IPv6, произвольная PKI/OID/path-length, независимые внешние peers и длительные переходы. BFD-only gate не гарантирован |
|
||||
| UX, доступность и производительность | UI226, ARIA мастеров251, новая проверка260 текущего runtime описана выше | Исправления260 и72 live состояния приняты; оставшиеся12 экранов и ручная screen-reader приёмка отдельно. Synthetic timing не field CWV, scoped axe не полная WCAG сертификация |
|
||||
| Границы выпуска | Датированный журнал, протоколы и локальные коммиты без push | Подготовить итоговый перечень принятых конфигураций/исключений для решения о выпуске; лабораторный успех не объявлять приёмкой всех конфигураций |
|
||||
|
||||
Portal offline JWT действует доexp после logout/role change; это принятая граница198, а не мгновенная revocation. Controlled Telegram unknown/retry201 не доказывает physical WAN lost ACK. User screenshots и vendor ReUI/theme сохранены.
|
||||
Portal offline JWT действует доexp после logout/role change; это записанная граница198, не мгновенная revocation. Controlled Telegram unknown/retry201 не доказывает физическую потерю ACK в WAN. Архивные записи ниже отражают состояние на свою дату; актуальный статус задают эта таблица и верхняя точка продолжения.
|
||||
|
||||
## Цель и обязательные требования
|
||||
|
||||
|
||||
+23
-2
@@ -1,6 +1,6 @@
|
||||
# UX/UI: журнал реализации
|
||||
|
||||
Актуальное продолжение 2026-10-15: 259 — реальный first issue и scheduler renewal приняты: trusted TLS сменил SHA-256, один logical leaf, старые native ID/fingerprint/private-key сохранены в архиве. Native/export/SSL/SQL/DNS восстановлены, private fixtures удалены. Runtime backend258/frontend251 healthy. Следующий260 — актуализировать остаток и выполнить профильную приёмку UX/доступности/производительности; этап12.5 открыт.
|
||||
Актуальное продолжение 2026-10-15: 260 — исправлены общие focus/target-size/contrast и BGP320 tabs; production frontend ux-accessibility260-final развёрнут, backend258 healthy.72 live WCAG/layout состояния прошли,59 уникальных E2E прошли в записанных итерациях; synthetic timing сохранён. Следующий261 — оставшиеся12 экранов и точные границы приёмки; этап12.5 открыт.
|
||||
|
||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
||||
|
||||
@@ -36,7 +36,28 @@
|
||||
| 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–259 в записанных границах. Реальная ротация/renewal259 и полный откат приняты. Runtime backend ux-certificates258/frontend ux-certificates251. Общая backend-регрессия257 и профиль сертификатов258 прошли. Следующий260 — актуальный остаток и профильная итоговая приёмка. |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная12.3.83; внешняя12.5 открыта, блоки1–260 в записанных границах. Ротация/renewal259 и откат приняты; общий UI260:72 live states passed. Runtime backend258/frontend260. Следующий261 — оставшиеся12 экранов и итоговые границы. |
|
||||
|
||||
## 12.5.260 — общая доступность, клавиатура и профильная скорость UI (2026-10-15)
|
||||
|
||||
Проведён read-only аудит18 разделов×2 темы×1440/320px:72 состояния текущего стенда/настоящий Portal UI, без response mocks. Baseline выявил23 состояния с axe нарушениями: недостаточная независимая pointer-area у focusable listbox-контейнера, контраст счётчиков SegmentedControl, raw type badges серверов и заголовка VXLAN. Document overflow0/JS errors0/API errors0; BGP320 имел2 внутренних переполнения main. Dashboard в baseline ещё содержал skeleton: его ранние timings не считаются готовностью данных. [Baseline](ux-ui/acceptance/ui260-baseline.json).
|
||||
|
||||
ServerTileRail сохраняет существующие Item/ScrollArea/DataGrid/44px варианты: один Tab stop на option, стрелки/Home/End перемещают selection и DOM focus, включая переход через страницу50. Root больше не конкурирует с вложенными option за фокус/target. Первые2 новые keyboard tests обнаружили remount GrePageContent при смене server URL; короткий10s keyboard intent восстанавливает фокус на новом выбранном элементе, только если пользователь не сфокусировал другой control, и игнорирует скрытый rail. Настройки устройств/URL selection/API не изменены.
|
||||
|
||||
TypeBadge переиспользует установленный ReUI Badge primary/info/success-light и text-foreground (как существующий rail), поскольку текущий dark success-foreground не прошёл4.5:1; vendor/style/theme не менялись, декоративные иконки скрыты от AT. У счётчиков снята opacity, заголовок VXLAN использует text-foreground. Вкладки BGP на узком экране имеют собственную горизонтальную прокрутку и не сжимаются. [Badge API](https://reui.io/docs/components/base/badge?ref=mcp), [preview](https://reui.io/components/badge?ref=mcp); audit checklist получен через MCP, выполнен в изменённых границах.
|
||||
|
||||
Локально initial57 E2E:55 passed/2 keyboard failed; после исправления remount rerun6 (2 keyboard+4 GRE/VXLAN) passed. Затем добавлены2 contrast tests всех3 типов серверов; final shared4 (2 contrast+2 keyboard) passed. Это59 уникальных принятых случаев; повторные прогоны не прибавляются к числу сценариев; initial отказ сохранён в сводке. Проверены серверные операции/отказы/RBAC/формы, BGP data states, GRE/VXLAN layout и backup layout. TypeScript/scoped ESLint passed. Production frontend build passed,528 source files matched; финальный образ ux-accessibility260-final развёрнут, backend258/DB/Portal сохранены.
|
||||
|
||||
Первый live recheck72 оставил2 color-contrast failures только на servers/dark; прочие70 прошли. После text-foreground final server recheck4 прошёл. Итоговое покрытие72 уникальных состояний объединено с явным происхождением результатов, а не выдано за76 разных случаев: axe WCAG2/2.1/2.2 tagged violations0, document/main overflow0, JS/API errors0, blocked writes0. Для data pages сначала ожидаются200 required endpoint responses; dashboard после этого skeleton0. Synthetic main-visible p50/p95=548/1052ms; required API response p50/p95=1033/2494ms. Это один Edge/headless lab run через SSH, без throttling, стандартный viewport snapshot; не field CWV/maximum load/полная ручная AT сертификация. [Полный протокол](ux-ui/acceptance/ui260-final.json), [сводка](ux-ui/acceptance/ui260-summary.json).
|
||||
|
||||
Визуально просмотрены screenshots servers1440 и BGP320. На BGP320 подписи KPI ещё сокращаются; эту отдельную проблему читаемости включить в261 вместе с оставшимися экранами, не считать её проверенной axe.
|
||||
|
||||
| Референс | Схема и экран |
|
||||
| --- | --- |
|
||||
| [ReUI Badge](https://reui.io/components/badge?ref=mcp) | Тип устройства → semantic variant → читаемая подпись; [servers1440](ux-ui/screens/ui260-servers.png) |
|
||||
| [ReUI Frame](https://reui.io/components/frame?ref=mcp) | Узкий экран → прокрутка вкладок → выбранный раздел; [BGP320](ux-ui/screens/ui260-bgp.png) |
|
||||
|
||||
Устройства не перенастраивались; новых native объектов/DNS/ACME не создавали. Текущая матрица исправлена: containers248 и ACME issue250/renewal259 приняты, старое отсутствие пакета/токена больше не записано как blocker. Семь групп описывают охват, не обязательные семь новых этапов. Следующий261 —12 оставшихся default screens: users/firewall/probes/terminal/data-collection/recursive-routes/route-optimizer/network-map/ip-ranges/asns/communities/domains; затем актуальные границы выпуска. Новая реализация CA revoke/MaxMind или внешняя публикация не объявляются выполненными.
|
||||
|
||||
## 12.5.259 — реальная ротация сертификата и подтверждённый откат (2026-10-15)
|
||||
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
# Оставшаяся внешняя приёмка — 2026-10-14
|
||||
# Оставшаяся внешняя приёмка — 2026-10-15
|
||||
|
||||
Актуальное продолжение 2026-10-15: 259 — реальный first issue и scheduler renewal приняты: trusted TLS сменил SHA-256, один logical leaf, старые native ID/fingerprint/private-key сохранены в архиве. Native/export/SSL/SQL/DNS восстановлены, private fixtures удалены. Runtime backend258/frontend251 healthy. Следующий260 — актуализировать остаток и выполнить профильную приёмку UX/доступности/производительности; этап12.5 открыт.
|
||||
Актуальное продолжение 2026-10-15: 260 — исправлены общие focus/target-size/contrast и BGP320 tabs; production frontend ux-accessibility260-final развёрнут, backend258 healthy.72 live WCAG/layout состояния прошли,59 уникальных E2E прошли в записанных итерациях; synthetic timing сохранён. Следующий261 — оставшиеся12 экранов и точные границы приёмки; этап12.5 открыт.
|
||||
|
||||
Это актуальная матрица после 12.5.259. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
Это актуальная матрица после 12.5.260. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-certificates258`, frontend `ux-certificates251`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-certificates258`, frontend `ux-accessibility260-final`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
|
||||
## 12.5.260 — общая доступность, клавиатура и профильная скорость UI (2026-10-15)
|
||||
|
||||
Проведён read-only аудит18 разделов×2 темы×1440/320px:72 состояния текущего стенда/настоящий Portal UI, без response mocks. Baseline выявил23 состояния с axe нарушениями: недостаточная независимая pointer-area у focusable listbox-контейнера, контраст счётчиков SegmentedControl, raw type badges серверов и заголовка VXLAN. Document overflow0/JS errors0/API errors0; BGP320 имел2 внутренних переполнения main. Dashboard в baseline ещё содержал skeleton: его ранние timings не считаются готовностью данных. [Baseline](ux-ui/acceptance/ui260-baseline.json).
|
||||
|
||||
ServerTileRail сохраняет существующие Item/ScrollArea/DataGrid/44px варианты: один Tab stop на option, стрелки/Home/End перемещают selection и DOM focus, включая переход через страницу50. Root больше не конкурирует с вложенными option за фокус/target. Первые2 новые keyboard tests обнаружили remount GrePageContent при смене server URL; короткий10s keyboard intent восстанавливает фокус на новом выбранном элементе, только если пользователь не сфокусировал другой control, и игнорирует скрытый rail. Настройки устройств/URL selection/API не изменены.
|
||||
|
||||
TypeBadge переиспользует установленный ReUI Badge primary/info/success-light и text-foreground (как существующий rail), поскольку текущий dark success-foreground не прошёл4.5:1; vendor/style/theme не менялись, декоративные иконки скрыты от AT. У счётчиков снята opacity, заголовок VXLAN использует text-foreground. Вкладки BGP на узком экране имеют собственную горизонтальную прокрутку и не сжимаются. [Badge API](https://reui.io/docs/components/base/badge?ref=mcp), [preview](https://reui.io/components/badge?ref=mcp); audit checklist получен через MCP, выполнен в изменённых границах.
|
||||
|
||||
Локально initial57 E2E:55 passed/2 keyboard failed; после исправления remount rerun6 (2 keyboard+4 GRE/VXLAN) passed. Затем добавлены2 contrast tests всех3 типов серверов; final shared4 (2 contrast+2 keyboard) passed. Это59 уникальных принятых случаев; повторные прогоны не прибавляются к числу сценариев; initial отказ сохранён в сводке. Проверены серверные операции/отказы/RBAC/формы, BGP data states, GRE/VXLAN layout и backup layout. TypeScript/scoped ESLint passed. Production frontend build passed,528 source files matched; финальный образ ux-accessibility260-final развёрнут, backend258/DB/Portal сохранены.
|
||||
|
||||
Первый live recheck72 оставил2 color-contrast failures только на servers/dark; прочие70 прошли. После text-foreground final server recheck4 прошёл. Итоговое покрытие72 уникальных состояний объединено с явным происхождением результатов, а не выдано за76 разных случаев: axe WCAG2/2.1/2.2 tagged violations0, document/main overflow0, JS/API errors0, blocked writes0. Для data pages сначала ожидаются200 required endpoint responses; dashboard после этого skeleton0. Synthetic main-visible p50/p95=548/1052ms; required API response p50/p95=1033/2494ms. Это один Edge/headless lab run через SSH, без throttling, стандартный viewport snapshot; не field CWV/maximum load/полная ручная AT сертификация. [Полный протокол](ux-ui/acceptance/ui260-final.json), [сводка](ux-ui/acceptance/ui260-summary.json).
|
||||
|
||||
Визуально просмотрены screenshots servers1440 и BGP320. На BGP320 подписи KPI ещё сокращаются; эту отдельную проблему читаемости включить в261 вместе с оставшимися экранами, не считать её проверенной axe.
|
||||
|
||||
| Референс | Схема и экран |
|
||||
| --- | --- |
|
||||
| [ReUI Badge](https://reui.io/components/badge?ref=mcp) | Тип устройства → semantic variant → читаемая подпись; [servers1440](ux-ui/screens/ui260-servers.png) |
|
||||
| [ReUI Frame](https://reui.io/components/frame?ref=mcp) | Узкий экран → прокрутка вкладок → выбранный раздел; [BGP320](ux-ui/screens/ui260-bgp.png) |
|
||||
|
||||
Устройства не перенастраивались; новых native объектов/DNS/ACME не создавали. Текущая матрица исправлена: containers248 и ACME issue250/renewal259 приняты, старое отсутствие пакета/токена больше не записано как blocker. Семь групп описывают охват, не обязательные семь новых этапов. Следующий261 —12 оставшихся default screens: users/firewall/probes/terminal/data-collection/recursive-routes/route-optimizer/network-map/ip-ranges/asns/communities/domains; затем актуальные границы выпуска. Новая реализация CA revoke/MaxMind или внешняя публикация не объявляются выполненными.
|
||||
|
||||
## 12.5.259 — реальная ротация сертификата и подтверждённый откат (2026-10-15)
|
||||
|
||||
@@ -316,21 +337,21 @@ Production backend/frontend ux-backups237 собраны и развёрнуты
|
||||
|
||||
Следующий238 фиксирует уже выполненное длительное REST-наблюдение. Затем — S3 restore через реальный UI в пределах собственного bridge и retention. Полная конфигурация CHR, arbitrary PKI, другие ОС, public IPv6, multi-instance и внешние jobs не приняты этим блоком.
|
||||
|
||||
## Остаток приёмки по группам — 2026-10-14
|
||||
## Остаток приёмки по группам — 2026-10-15
|
||||
|
||||
Этапы0–11 завершены в локальных границах. Этап12.5 открыт: ниже7 групп проверок, а не7 обещанных окон или точное число тестов. Детали и исторические ограничения сохраняются в [матрице](ux-ui-remaining-acceptance.md).
|
||||
Этапы0–11 завершены в локальных границах. Этап12.5 открыт. Ниже семь групп охвата; это не семь обязательных новых этапов и не обещание числа окон. Принятые сценарии не запускать заново без изменения кода или найденного риска. Для расширения стенда отдельно нужны перечисленные условия.
|
||||
|
||||
| Группа | Что уже подтверждено | Оставшийся результат / что требуется |
|
||||
| Группа | Подтверждено | Открытый объём / условие |
|
||||
| --- | --- | --- |
|
||||
| REST и достоверность данных | Ошибки OSPF/BFD/BGP/GRE/WAN215–221 явные; latest API smoke222 прошёл | Correlated236:384 samples/40 diagnostics без отказов; current policies/address/load исправны.828/304 retained REST sessions и vendor inactive-session fix — гипотеза; 238:763.844s/624samples/24diagnostics все200, API records изменились без отказа; причинность401/500 и version control остаются открытыми |
|
||||
| Клиентские ОС и внешняя сеть | CHR↔CHR/Linux, private IPv4/ULA и synthetic IPv6/IPFIX; Android full-tunnel/reconnect227 подтверждены пользователем | Windows/macOS/iOS VPN, расширенная Android приёмка, независимые WAN/multi-WAN и public IPv6; public IPv6 на текущем стенде отсутствует |
|
||||
| Масштаб и длительная устойчивость | Контролируемые lock/dedupe/loss/recovery и короткие native проверки | Несколько backend/worker/writers, длительный rekey/REST/traffic, physical disconnect и maximum throughput |
|
||||
| Внешние хранилища и jobs | Portal/Telegram195–201; native S3230, persistence235, missing UX237, bridge-only live restore239, isolated retention240; real GeoIP mirror242; availability243 | Large native export только HTTP fixture; migration buckets/multi-instance. Containers:feature=true, package не обнаружен,/container400 — нужны package/lifecycle. ACME:no token/zone, нужен test-domain/scoped Cloudflare/staging. Gitea:local dry-run принят; publishing CI не запускался (no push). Direct licensed MaxMind не реализован |
|
||||
| Расширенные сетевые конфигурации | GRE/WG/VXLAN/IPsec/OSPF/BFD/BGP сценарии и точные границы записаны | from-pool/EUI-64/multiple-address import/export, outer IPv6, произвольная PKI/OID/policy/path-length и длительные переходы; BFD-only gate гарантией не стал |
|
||||
| Общий UX, доступность и производительность | Полная UI192 исторически; latest backend25/UI52, scoped contrast/live218 и keyboard206 | Свежая UI226 принята в записанных границах; профильный performance и ручная accessibility/screen-reader приёмка; scoped axe не полный WCAG |
|
||||
| Границы выпуска | План/доказательства/датированные блоки сохранены; коммиты локальные | Определить требуемые конфигурации и принять результат в этих границах; невыполненные варианты остаются явными ограничениями |
|
||||
| REST и достоверность данных | Нативные наблюдения236/238; ограничение shared REST pools257,12 параллельных чтений двух CHR и25 backend-профилей прошли | Длительное наблюдение после transport fix; историческая причинность всех401/500 и сравнение версий RouterOS полностью не доказаны |
|
||||
| Клиентские ОС и внешняя сеть | CHR↔CHR/Linux; Android StrongSwan full-tunnel и reconnect227 подтверждены пользователем | Windows/macOS/iOS и другие клиенты; независимые WAN/multi-WAN. Публичного IPv6 на предоставленном стенде нет — проверка требует другого подключения |
|
||||
| Масштаб и устойчивость | Контролируемые PG lock/dedupe/loss/recovery, отдельные multi-process сценарии194/200/201 | Несколько полноценных backend/worker со всеми writers, длительный rekey/traffic, физические потери WAN/DB и maximum throughput |
|
||||
| Хранилища и внешние jobs | Реальные Portal/Telegram195–201, Selectel S3/limited bridge restore/retention228–240, GeoIP mirror242; контейнеры248; production ACME250 и реальное renewal/rotation259 с trusted TLS/cleanup | Large native export/migration buckets/multi-instance S3; CA revocation отдельно не принят. Gitea publishing CI не запускался: действует no push. Direct licensed MaxMind отсутствует в текущей реализации |
|
||||
| Расширенные сетевые конфигурации | GRE/WG/VXLAN/IPsec/OSPF/BFD/BGP в записанных сценариях, включая private IPv6 | from-pool/EUI-64/multiple-address import/export, outer IPv6, произвольная PKI/OID/path-length, независимые внешние peers и длительные переходы. BFD-only gate не гарантирован |
|
||||
| UX, доступность и производительность | UI226, ARIA мастеров251, новая проверка260 текущего runtime описана выше | Исправления260 и72 live состояния приняты; оставшиеся12 экранов и ручная screen-reader приёмка отдельно. Synthetic timing не field CWV, scoped axe не полная WCAG сертификация |
|
||||
| Границы выпуска | Датированный журнал, протоколы и локальные коммиты без push | Подготовить итоговый перечень принятых конфигураций/исключений для решения о выпуске; лабораторный успех не объявлять приёмкой всех конфигураций |
|
||||
|
||||
Portal offline JWT действует доexp после logout/role change; это принятая граница198, а не мгновенная revocation. Controlled Telegram unknown/retry201 не доказывает physical WAN lost ACK. User screenshots и vendor ReUI/theme сохранены.
|
||||
Portal offline JWT действует доexp после logout/role change; это записанная граница198, не мгновенная revocation. Controlled Telegram unknown/retry201 не доказывает физическую потерю ACK в WAN. Архивные записи ниже отражают состояние на свою дату; актуальный статус задают эта таблица и верхняя точка продолжения.
|
||||
|
||||
| Раздел | Подтверждено | Осталось / условие завершения |
|
||||
| --- | --- | --- |
|
||||
@@ -345,14 +366,14 @@ Portal offline JWT действует доexp после logout/role change; э
|
||||
| BGP | Established83, exact /32 exchange/API/UI/administrative withdrawal92, BFD primary GRE loss/alternate/recovery93; basic native IPv6 eBGP/AFI/NLRI/Ping/withdrawal/API/UI155; native IPv6 BGP/BFD scoped withdrawal/recovery160; private4-byte ASN/as4/API/UI162 | Hold timer/TCP loss безBFD принят104; packet loss в переходе, разные внешние peers/WAN policy, basic BFDv6 принят160; IPv6 outer/public WAN/scale. Два логических пути используют тот же public WAN; physical redundancy не принята |
|
||||
| Recursive routes | Production PUT/change/own history restore88, disabled guards, partial/unknown/applied outcomes, live формы/контекст | Автоматический rollback/retry, durable receipts и несколько экземпляров отсутствуют; PUT не является idempotent noop. Полная произвольная legacy catalog миграция не принята |
|
||||
| Diagnostics/terminal | Allowlist/native reads, Ping/trace/route/MTU/DNS, actual avg-rtt, disabled guards, JWT/live PC/mobile89; real default memory tail/log limits/API/UI133 | DNS использует backend resolver; MTU — ограниченная оценка, не гарантированный максимум. Stop ожидания не доказывает остановку RouterOS команды; arbitrary CLI и physical disconnect не приняты |
|
||||
| Portal/интеграции | Portal1.3.1/issuer/login/RBAC/expiry/context195–199, actual Telegram201; Selectel S3 native/API/live limited restore/retention228–240; real GeoIP mirror242 | Offline JWT сохраняетдоступ доexp; S3 large-native/migration/multi-instance отдельно; containers lifecycle248 принят в записанных границах; ACME issuance250 принят; automatic renewal/CA revoke и remote Gitea publishing не приняты; direct licensed MaxMind не реализован. ClientOS/scale/WAN отдельно |
|
||||
| Portal/интеграции | Portal1.3.1/issuer/login/RBAC/expiry/context195–199, actual Telegram201; Selectel S3 native/API/live limited restore/retention228–240; real GeoIP mirror242 | Offline JWT сохраняетдоступ доexp; S3 large-native/migration/multi-instance отдельно; containers lifecycle248 принят в записанных границах; ACME issuance250 принят; automatic renewal/rotation259 приняты в конкретном сценарии; CA revoke и remote Gitea publishing не приняты; direct licensed MaxMind не реализован. ClientOS/scale/WAN отдельно |
|
||||
| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar passed149; Sessions labels passed150; application GRE static IPv6 lifecycle151 passed; WireGuard static IPv6 lifecycle152 и disabled SQL/API gates passed; OSPF/BFD boundary/warning153 passed; OSPFv3 routes154 accepted in recorded bounds; BGP IPv6 basic155 passed; bigint ID/SQL guards156 passed; общая UI1260/context221/backend24 regression157 passed; IPv6 BFD correlation158 profile/deployed passed; native OSPFv3/BFDv6 boundary/recovery159 passed; BGP IPv6/BFD withdrawal/recovery160 passed; selected BGP/alert unknown guards161 replay/profiles/deployed passed (SQL stability skipped); private4-byte ASN162 native/API/UI passed; duplicate BGP alert keys163 replay/profiles/deployed passed; nullable selected BGP164 replay/profiles/API/UI passed; collector165 parallel/current guards/read-only PG passed; positive partial/error SQL166/full24 with PG passed (optional network SQL not repeated); GRE/BGP freshness167/SQL8 passed; other source audit168 выявил identity/disabled/freshness gaps; локальный server/traffic identity guard169 passed без SQL/deploy, source/per-probe/freshness170/ownSQL16/compiled1008/deployed passed; source diagnostics171/42E2E/ownSQL metadata/live2 passed; monitoring history172/ownSQL26/compiled1020/deployed passed; Ping known/error173/ownHTTP-SQL8/85uniqueE2E/native4/4/deployed passed; traffic confidence174 baseline reproduced/local guard passed; owned SQL9/9358ms/deploy175/1024compiled/live smoke passed; legacy history/provenance open; compound RTT176 local/native replay2 passed, SQL10/deploy177/1024compiled/live smoke passed; history178/legacy provenance/current TTL/manual truth/SQL9+14/35unique E2E/native GET/deployed passed; live179 complete/disable-config/bounded fallback/ownSQL10/nativeSSE2/liveUI2 passed; volume180 exact/unit marker/ownSQL6/18E2E/API/liveUI/deployed passed; binding status181 native/ownSQL9/8E2E/deployed passed; peer atomicity182 ownSQL3/native/API/UI passed; peer failure warning183 ownSQL2/4E2E/deployed passed; inactive history184 ownSQL2/deployed passed; gap185/identity186/restore187/wave188/sourceguard189/confirmed peak190 passed; aggregate peaks191, request dedupe193, cross-process lock194/200, full regression192 (1286 unique E2E и25-й профиль privacy196 scoped), Portal195–199/Telegram201 passed; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию |
|
||||
|
||||
Доказательства и точные ограничения: [полная история этапа12](ux-ui-stage-12.md), [внешний протокол](ux-ui-external-acceptance.md), [BGP93](ux-ui/acceptance/bgp-failover-2026-10-10.json), [паспорт стенда](ux-ui-test-server.md). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение.
|
||||
|
||||
## Следующая последовательность — 2026-10-14
|
||||
## Следующая последовательность — 2026-10-15
|
||||
|
||||
1. **Приёмка260:** обновить актуальный остаток без устаревших blockers; профильный UX/a11y/performance контроль текущего runtime. Реальная ротация и откат259 приняты.
|
||||
1. **Приёмка261:**12 оставшихся default screens с real Portal/API и точным read-only guard; профильный audit/layout/performance. Сертификаты259 и UI260 приняты в записанных границах.
|
||||
2. **Профильный контроль:** для новых изменений выполнять затронутые SQL/API/UI проверки. Общая регрессия192 выполнена; не повторять её без изменений общей основы или нового основания.
|
||||
3. **Внешние зависимости:** Portal issuer/login/revoke, Telegram bot/chat/разрешённое сообщение, клиентские ОС, независимые WAN/public IPv6 и scale/multi-instance — отдельные группы по таблице выше. Ограничения текущего стенда не превращать в обещание приёмки всех конфигураций.
|
||||
|
||||
|
||||
+22
-1
@@ -1,6 +1,6 @@
|
||||
# Этап 12: итоговая приёмка UX/UI
|
||||
|
||||
Актуальное продолжение 2026-10-15: 259 — реальный first issue и scheduler renewal приняты: trusted TLS сменил SHA-256, один logical leaf, старые native ID/fingerprint/private-key сохранены в архиве. Native/export/SSL/SQL/DNS восстановлены, private fixtures удалены. Runtime backend258/frontend251 healthy. Следующий260 — актуализировать остаток и выполнить профильную приёмку UX/доступности/производительности; этап12.5 открыт.
|
||||
Актуальное продолжение 2026-10-15: 260 — исправлены общие focus/target-size/contrast и BGP320 tabs; production frontend ux-accessibility260-final развёрнут, backend258 healthy.72 live WCAG/layout состояния прошли,59 уникальных E2E прошли в записанных итерациях; synthetic timing сохранён. Следующий261 — оставшиеся12 экранов и точные границы приёмки; этап12.5 открыт.
|
||||
|
||||
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
|
||||
|
||||
@@ -4152,6 +4152,27 @@ Real Portal/browser API, provider local verified: создан один job ро
|
||||
Runtime backend ux-backup234/frontend ux-routing218 healthy; новое application image для volume-only fix не требовалось. Пароли CHR не менялись, S3 mutations0 в235, configs body/JWT не опубликованы, private local manifest удалён. Исходные10 missing local files не восстановлены и records не удалены. Force-recreate — не host reboot/power loss/fsync/DR приёмка. Далее236 — UX недоступных local files, затем version retention границы и оставшиеся7 группы матрицы; multi-instance guard не принимается. Этап12.5 открыт.
|
||||
|
||||
|
||||
## 12.5.260 — общая доступность, клавиатура и профильная скорость UI (2026-10-15)
|
||||
|
||||
Проведён read-only аудит18 разделов×2 темы×1440/320px:72 состояния текущего стенда/настоящий Portal UI, без response mocks. Baseline выявил23 состояния с axe нарушениями: недостаточная независимая pointer-area у focusable listbox-контейнера, контраст счётчиков SegmentedControl, raw type badges серверов и заголовка VXLAN. Document overflow0/JS errors0/API errors0; BGP320 имел2 внутренних переполнения main. Dashboard в baseline ещё содержал skeleton: его ранние timings не считаются готовностью данных. [Baseline](ux-ui/acceptance/ui260-baseline.json).
|
||||
|
||||
ServerTileRail сохраняет существующие Item/ScrollArea/DataGrid/44px варианты: один Tab stop на option, стрелки/Home/End перемещают selection и DOM focus, включая переход через страницу50. Root больше не конкурирует с вложенными option за фокус/target. Первые2 новые keyboard tests обнаружили remount GrePageContent при смене server URL; короткий10s keyboard intent восстанавливает фокус на новом выбранном элементе, только если пользователь не сфокусировал другой control, и игнорирует скрытый rail. Настройки устройств/URL selection/API не изменены.
|
||||
|
||||
TypeBadge переиспользует установленный ReUI Badge primary/info/success-light и text-foreground (как существующий rail), поскольку текущий dark success-foreground не прошёл4.5:1; vendor/style/theme не менялись, декоративные иконки скрыты от AT. У счётчиков снята opacity, заголовок VXLAN использует text-foreground. Вкладки BGP на узком экране имеют собственную горизонтальную прокрутку и не сжимаются. [Badge API](https://reui.io/docs/components/base/badge?ref=mcp), [preview](https://reui.io/components/badge?ref=mcp); audit checklist получен через MCP, выполнен в изменённых границах.
|
||||
|
||||
Локально initial57 E2E:55 passed/2 keyboard failed; после исправления remount rerun6 (2 keyboard+4 GRE/VXLAN) passed. Затем добавлены2 contrast tests всех3 типов серверов; final shared4 (2 contrast+2 keyboard) passed. Это59 уникальных принятых случаев; повторные прогоны не прибавляются к числу сценариев; initial отказ сохранён в сводке. Проверены серверные операции/отказы/RBAC/формы, BGP data states, GRE/VXLAN layout и backup layout. TypeScript/scoped ESLint passed. Production frontend build passed,528 source files matched; финальный образ ux-accessibility260-final развёрнут, backend258/DB/Portal сохранены.
|
||||
|
||||
Первый live recheck72 оставил2 color-contrast failures только на servers/dark; прочие70 прошли. После text-foreground final server recheck4 прошёл. Итоговое покрытие72 уникальных состояний объединено с явным происхождением результатов, а не выдано за76 разных случаев: axe WCAG2/2.1/2.2 tagged violations0, document/main overflow0, JS/API errors0, blocked writes0. Для data pages сначала ожидаются200 required endpoint responses; dashboard после этого skeleton0. Synthetic main-visible p50/p95=548/1052ms; required API response p50/p95=1033/2494ms. Это один Edge/headless lab run через SSH, без throttling, стандартный viewport snapshot; не field CWV/maximum load/полная ручная AT сертификация. [Полный протокол](ux-ui/acceptance/ui260-final.json), [сводка](ux-ui/acceptance/ui260-summary.json).
|
||||
|
||||
Визуально просмотрены screenshots servers1440 и BGP320. На BGP320 подписи KPI ещё сокращаются; эту отдельную проблему читаемости включить в261 вместе с оставшимися экранами, не считать её проверенной axe.
|
||||
|
||||
| Референс | Схема и экран |
|
||||
| --- | --- |
|
||||
| [ReUI Badge](https://reui.io/components/badge?ref=mcp) | Тип устройства → semantic variant → читаемая подпись; [servers1440](ux-ui/screens/ui260-servers.png) |
|
||||
| [ReUI Frame](https://reui.io/components/frame?ref=mcp) | Узкий экран → прокрутка вкладок → выбранный раздел; [BGP320](ux-ui/screens/ui260-bgp.png) |
|
||||
|
||||
Устройства не перенастраивались; новых native объектов/DNS/ACME не создавали. Текущая матрица исправлена: containers248 и ACME issue250/renewal259 приняты, старое отсутствие пакета/токена больше не записано как blocker. Семь групп описывают охват, не обязательные семь новых этапов. Следующий261 —12 оставшихся default screens: users/firewall/probes/terminal/data-collection/recursive-routes/route-optimizer/network-map/ip-ranges/asns/communities/domains; затем актуальные границы выпуска. Новая реализация CA revoke/MaxMind или внешняя публикация не объявляются выполненными.
|
||||
|
||||
## 12.5.259 — реальная ротация сертификата и подтверждённый откат (2026-10-15)
|
||||
|
||||
По сохранённому разрешению254 выполнен тот же exact mmt253-8e62a941.pngs.top на104.165.244.7. Перед началом снят свежий приватный native/export/SSL/files/SQL baseline; own certificate/files/jobs/DNS0, Cloudflare token/zone/account проверены. Renewal временно выключен. Подготовленный backend258 активирован; frontend251/Portal/DB сохранены, backend healthy. [Развёртывание](ux-ui/acceptance/acme259-deployment.json).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Тестовый сервер UX/UI — 2026-10-10
|
||||
|
||||
Актуальное продолжение 2026-10-15: 259 — реальный first issue и scheduler renewal приняты: trusted TLS сменил SHA-256, один logical leaf, старые native ID/fingerprint/private-key сохранены в архиве. Native/export/SSL/SQL/DNS восстановлены, private fixtures удалены. Runtime backend258/frontend251 healthy. Следующий260 — актуализировать остаток и выполнить профильную приёмку UX/доступности/производительности; этап12.5 открыт.
|
||||
Актуальное продолжение 2026-10-15: 260 — исправлены общие focus/target-size/contrast и BGP320 tabs; production frontend ux-accessibility260-final развёрнут, backend258 healthy.72 live WCAG/layout состояния прошли,59 уникальных E2E прошли в записанных итерациях; synthetic timing сохранён. Следующий261 — оставшиеся12 экранов и точные границы приёмки; этап12.5 открыт.
|
||||
|
||||
| Параметр | Проверенное состояние |
|
||||
| --- | --- |
|
||||
@@ -14,7 +14,7 @@
|
||||
| PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
|
||||
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
|
||||
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска |
|
||||
| Образы приложения | mmapp-test/backend:ux-certificates258 и mmapp-test/frontend:ux-certificates251; backend healthy, проверено2026-10-15 в259 |
|
||||
| Образы приложения | mmapp-test/backend:ux-certificates258 и mmapp-test/frontend:ux-accessibility260-final; backend healthy, проверено2026-10-15 в260 |
|
||||
|
||||
Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example).
|
||||
|
||||
@@ -34,6 +34,27 @@ OSPF84: real GRE Full adjacency/private routes/Ping3/3 и4 live PC/320px URL/bro
|
||||
|
||||
BFD85: Up/Full/own route/Ping,4 live UI и loss/withdrawal/recovery приняты; exact own cleanup8 коллекций×2 passed. Withdrawal5,245ms не подтверждает sub-second BFD timing; этот вопрос открыт. Runtime routing83 без новой сборки. [Отчёт](ux-ui/acceptance/ospf-bfd-loss-2026-10-09.json).
|
||||
|
||||
## 12.5.260 — общая доступность, клавиатура и профильная скорость UI (2026-10-15)
|
||||
|
||||
Проведён read-only аудит18 разделов×2 темы×1440/320px:72 состояния текущего стенда/настоящий Portal UI, без response mocks. Baseline выявил23 состояния с axe нарушениями: недостаточная независимая pointer-area у focusable listbox-контейнера, контраст счётчиков SegmentedControl, raw type badges серверов и заголовка VXLAN. Document overflow0/JS errors0/API errors0; BGP320 имел2 внутренних переполнения main. Dashboard в baseline ещё содержал skeleton: его ранние timings не считаются готовностью данных. [Baseline](ux-ui/acceptance/ui260-baseline.json).
|
||||
|
||||
ServerTileRail сохраняет существующие Item/ScrollArea/DataGrid/44px варианты: один Tab stop на option, стрелки/Home/End перемещают selection и DOM focus, включая переход через страницу50. Root больше не конкурирует с вложенными option за фокус/target. Первые2 новые keyboard tests обнаружили remount GrePageContent при смене server URL; короткий10s keyboard intent восстанавливает фокус на новом выбранном элементе, только если пользователь не сфокусировал другой control, и игнорирует скрытый rail. Настройки устройств/URL selection/API не изменены.
|
||||
|
||||
TypeBadge переиспользует установленный ReUI Badge primary/info/success-light и text-foreground (как существующий rail), поскольку текущий dark success-foreground не прошёл4.5:1; vendor/style/theme не менялись, декоративные иконки скрыты от AT. У счётчиков снята opacity, заголовок VXLAN использует text-foreground. Вкладки BGP на узком экране имеют собственную горизонтальную прокрутку и не сжимаются. [Badge API](https://reui.io/docs/components/base/badge?ref=mcp), [preview](https://reui.io/components/badge?ref=mcp); audit checklist получен через MCP, выполнен в изменённых границах.
|
||||
|
||||
Локально initial57 E2E:55 passed/2 keyboard failed; после исправления remount rerun6 (2 keyboard+4 GRE/VXLAN) passed. Затем добавлены2 contrast tests всех3 типов серверов; final shared4 (2 contrast+2 keyboard) passed. Это59 уникальных принятых случаев; повторные прогоны не прибавляются к числу сценариев; initial отказ сохранён в сводке. Проверены серверные операции/отказы/RBAC/формы, BGP data states, GRE/VXLAN layout и backup layout. TypeScript/scoped ESLint passed. Production frontend build passed,528 source files matched; финальный образ ux-accessibility260-final развёрнут, backend258/DB/Portal сохранены.
|
||||
|
||||
Первый live recheck72 оставил2 color-contrast failures только на servers/dark; прочие70 прошли. После text-foreground final server recheck4 прошёл. Итоговое покрытие72 уникальных состояний объединено с явным происхождением результатов, а не выдано за76 разных случаев: axe WCAG2/2.1/2.2 tagged violations0, document/main overflow0, JS/API errors0, blocked writes0. Для data pages сначала ожидаются200 required endpoint responses; dashboard после этого skeleton0. Synthetic main-visible p50/p95=548/1052ms; required API response p50/p95=1033/2494ms. Это один Edge/headless lab run через SSH, без throttling, стандартный viewport snapshot; не field CWV/maximum load/полная ручная AT сертификация. [Полный протокол](ux-ui/acceptance/ui260-final.json), [сводка](ux-ui/acceptance/ui260-summary.json).
|
||||
|
||||
Визуально просмотрены screenshots servers1440 и BGP320. На BGP320 подписи KPI ещё сокращаются; эту отдельную проблему читаемости включить в261 вместе с оставшимися экранами, не считать её проверенной axe.
|
||||
|
||||
| Референс | Схема и экран |
|
||||
| --- | --- |
|
||||
| [ReUI Badge](https://reui.io/components/badge?ref=mcp) | Тип устройства → semantic variant → читаемая подпись; [servers1440](ux-ui/screens/ui260-servers.png) |
|
||||
| [ReUI Frame](https://reui.io/components/frame?ref=mcp) | Узкий экран → прокрутка вкладок → выбранный раздел; [BGP320](ux-ui/screens/ui260-bgp.png) |
|
||||
|
||||
Устройства не перенастраивались; новых native объектов/DNS/ACME не создавали. Текущая матрица исправлена: containers248 и ACME issue250/renewal259 приняты, старое отсутствие пакета/токена больше не записано как blocker. Семь групп описывают охват, не обязательные семь новых этапов. Следующий261 —12 оставшихся default screens: users/firewall/probes/terminal/data-collection/recursive-routes/route-optimizer/network-map/ip-ranges/asns/communities/domains; затем актуальные границы выпуска. Новая реализация CA revoke/MaxMind или внешняя публикация не объявляются выполненными.
|
||||
|
||||
## 12.5.259 — реальная ротация сертификата и подтверждённый откат (2026-10-15)
|
||||
|
||||
По сохранённому разрешению254 выполнен тот же exact mmt253-8e62a941.pngs.top на104.165.244.7. Перед началом снят свежий приватный native/export/SSL/files/SQL baseline; own certificate/files/jobs/DNS0, Cloudflare token/zone/account проверены. Renewal временно выключен. Подготовленный backend258 активирован; frontend251/Portal/DB сохранены, backend healthy. [Развёртывание](ux-ui/acceptance/acme259-deployment.json).
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"date": "2026-10-15",
|
||||
"block": "12.5.260",
|
||||
"productionBuild": "passed",
|
||||
"sourceMatched": 528,
|
||||
"services": {
|
||||
"backend": {
|
||||
"image": "mmapp-test/backend:ux-certificates258",
|
||||
"imageId": "sha256:984684709e619972bae8657208e3150d12f1c4f2b5642fecf5286a5d87e9fe71",
|
||||
"status": "running",
|
||||
"health": "healthy"
|
||||
},
|
||||
"frontend": {
|
||||
"image": "mmapp-test/frontend:ux-accessibility260-final",
|
||||
"imageId": "sha256:0b7b12cfa0e64a021c8c77a3362cc54488c6aea75269f0c2ae2a0cb0b4b5192a",
|
||||
"status": "running",
|
||||
"health": null
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"date": "2026-10-15",
|
||||
"block": "12.5.260",
|
||||
"origin": "http://localhost:3000",
|
||||
"realPortalUiLogin": true,
|
||||
"responseMocks": false,
|
||||
"deviceWritesPermitted": false,
|
||||
"cases": [
|
||||
{
|
||||
"theme": "dark",
|
||||
"path": "/servers",
|
||||
"width": 1440,
|
||||
"pageErrors": [],
|
||||
"apiErrors": [],
|
||||
"mainVisibleMs": 760,
|
||||
"requiredReads": [
|
||||
"/api/servers"
|
||||
],
|
||||
"dataResponsesMs": 1639,
|
||||
"layout": {
|
||||
"overflow": false,
|
||||
"mainOverflow": false,
|
||||
"theme": "dark",
|
||||
"pendingSkeletons": 0
|
||||
},
|
||||
"performance": {
|
||||
"lcp": 1232,
|
||||
"cls": 0.000044211847752700614,
|
||||
"longTasks": 0,
|
||||
"longTaskMs": 0,
|
||||
"ttfbMs": 144.09999999776483,
|
||||
"domInteractiveMs": 413.5,
|
||||
"durationMs": 740.3000000007451
|
||||
},
|
||||
"violations": [],
|
||||
"auditCompleted": true
|
||||
},
|
||||
{
|
||||
"theme": "dark",
|
||||
"path": "/servers",
|
||||
"width": 320,
|
||||
"pageErrors": [],
|
||||
"apiErrors": [],
|
||||
"mainVisibleMs": 480,
|
||||
"requiredReads": [
|
||||
"/api/servers"
|
||||
],
|
||||
"dataResponsesMs": 585,
|
||||
"layout": {
|
||||
"overflow": false,
|
||||
"mainOverflow": false,
|
||||
"theme": "dark",
|
||||
"pendingSkeletons": 0
|
||||
},
|
||||
"performance": {
|
||||
"lcp": 452,
|
||||
"cls": 0.000062321875,
|
||||
"longTasks": 0,
|
||||
"longTaskMs": 0,
|
||||
"ttfbMs": 55.30000000074506,
|
||||
"domInteractiveMs": 157.19999999925494,
|
||||
"durationMs": 389.29999999701977
|
||||
},
|
||||
"violations": [],
|
||||
"auditCompleted": true
|
||||
},
|
||||
{
|
||||
"theme": "light",
|
||||
"path": "/servers",
|
||||
"width": 1440,
|
||||
"pageErrors": [],
|
||||
"apiErrors": [],
|
||||
"mainVisibleMs": 1150,
|
||||
"requiredReads": [
|
||||
"/api/servers"
|
||||
],
|
||||
"dataResponsesMs": 1150,
|
||||
"layout": {
|
||||
"overflow": false,
|
||||
"mainOverflow": false,
|
||||
"theme": "light",
|
||||
"pendingSkeletons": 0
|
||||
},
|
||||
"performance": {
|
||||
"lcp": 988,
|
||||
"cls": 0.000044211847752700614,
|
||||
"longTasks": 0,
|
||||
"longTaskMs": 0,
|
||||
"ttfbMs": 115.19999999925494,
|
||||
"domInteractiveMs": 323.6000000014901,
|
||||
"durationMs": 730.5
|
||||
},
|
||||
"violations": [],
|
||||
"auditCompleted": true
|
||||
},
|
||||
{
|
||||
"theme": "light",
|
||||
"path": "/servers",
|
||||
"width": 320,
|
||||
"pageErrors": [],
|
||||
"apiErrors": [],
|
||||
"mainVisibleMs": 1052,
|
||||
"requiredReads": [
|
||||
"/api/servers"
|
||||
],
|
||||
"dataResponsesMs": 1052,
|
||||
"layout": {
|
||||
"overflow": false,
|
||||
"mainOverflow": false,
|
||||
"theme": "light",
|
||||
"pendingSkeletons": 0
|
||||
},
|
||||
"performance": {
|
||||
"lcp": 632,
|
||||
"cls": 0.000062321875,
|
||||
"longTasks": 0,
|
||||
"longTaskMs": 0,
|
||||
"ttfbMs": 63,
|
||||
"domInteractiveMs": 236,
|
||||
"durationMs": 567
|
||||
},
|
||||
"violations": [],
|
||||
"auditCompleted": true
|
||||
}
|
||||
],
|
||||
"blockedWrites": [],
|
||||
"readOnlyPosts": [],
|
||||
"bounds": [
|
||||
"Default rendered state only; data can still be refreshing",
|
||||
"Synthetic lab timing over SSH, no CPU/network throttling; not field Core Web Vitals",
|
||||
"Automated WCAG-tagged checks, not manual screen-reader certification"
|
||||
],
|
||||
"completed": true,
|
||||
"passed": true
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"date": "2026-10-15",
|
||||
"block": "12.5.260",
|
||||
"runtime": {
|
||||
"backend": "ux-certificates258",
|
||||
"frontend": "ux-accessibility260-final"
|
||||
},
|
||||
"baseline": {
|
||||
"cases": 72,
|
||||
"violatingCases": 23,
|
||||
"documentOverflow": 0,
|
||||
"internalBgpOverflowCases": 2
|
||||
},
|
||||
"final": {
|
||||
"cases": 72,
|
||||
"axeViolations": 0,
|
||||
"documentOverflow": 0,
|
||||
"mainOverflow": 0,
|
||||
"pageErrors": 0,
|
||||
"apiErrors": 0,
|
||||
"blockedWrites": 0,
|
||||
"pendingSkeletonCases": 0
|
||||
},
|
||||
"timings": {
|
||||
"mainVisible": {
|
||||
"count": 72,
|
||||
"p50Ms": 548,
|
||||
"p95Ms": 1052,
|
||||
"maxMs": 1243
|
||||
},
|
||||
"requiredApiResponses": {
|
||||
"count": 64,
|
||||
"p50Ms": 1033,
|
||||
"p95Ms": 2494,
|
||||
"maxMs": 3482
|
||||
}
|
||||
},
|
||||
"local": {
|
||||
"initialE2E": {
|
||||
"passed": 55,
|
||||
"failed": 2
|
||||
},
|
||||
"afterFocusRemountFix": {
|
||||
"passed": 6,
|
||||
"failed": 0
|
||||
},
|
||||
"finalSharedCases": {
|
||||
"passed": 4,
|
||||
"failed": 0
|
||||
},
|
||||
"uniqueCasesThatPassed": 59,
|
||||
"typecheck": "passed",
|
||||
"scopedLint": "passed",
|
||||
"productionBuild": "passed",
|
||||
"sourceManifestMatched": 528
|
||||
},
|
||||
"bounds": [
|
||||
"Default rendered state only; data can still be refreshing",
|
||||
"Synthetic lab timing over SSH, no CPU/network throttling; not field Core Web Vitals",
|
||||
"Automated WCAG-tagged checks, not manual screen-reader certification"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,68 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
import axe from "axe-core"
|
||||
import { serverReadSchema } from "../../packages/contracts/src/servers"
|
||||
|
||||
for (const theme of ["light", "dark"]) test(`Server type badges: all types have readable contrast/${theme}`, async ({ page }) => {
|
||||
await page.addInitScript(value => localStorage.setItem("rl-theme", value), theme)
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
const servers = ["home-router", "exit-node", "jump-host"].map((type, i) => serverReadSchema.parse({
|
||||
id: i + 1, name: `Контраст ${i + 1}`, host: `192.0.2.${i + 1}`, port: 443,
|
||||
username: "fixture", password: "fixture", useSsl: true, verifySsl: true, type, site: "TEST", country: "RU", asn: "", comment: "",
|
||||
enabled: true, lanSubnet: "", wanUplinks: [], createdAt: "2026-10-15T00:00:00Z", updatedAt: "2026-10-15T00:00:00Z",
|
||||
status: "online", latency: 1, os: "7.24.5", model: "CHR", uptime: null, cpuLoad: null,
|
||||
freeMemory: null, totalMemory: null, identityName: null, sessions: 0, polledAt: null,
|
||||
}))
|
||||
await page.route("**/api/**", route => route.fulfill({ json: new URL(route.request().url()).pathname === "/api/servers" ? servers
|
||||
: new URL(route.request().url()).pathname === "/api/auth/config" ? { required: false } : {} }))
|
||||
await page.goto("/servers")
|
||||
for (const name of ["Home Router", "Exit Node", "JumpHost"]) await expect(page.getByRole("cell", { name, exact: true })).toBeVisible()
|
||||
await page.addScriptTag({ content: axe.source })
|
||||
const violations = await page.evaluate(async () => (await (window as unknown as { axe: typeof axe }).axe.run("#main-content", {
|
||||
runOnly: { type: "rule", values: ["color-contrast"] },
|
||||
})).violations.map(v => ({ id: v.id, targets: v.nodes.map(n => n.target) })))
|
||||
expect(violations).toEqual([])
|
||||
})
|
||||
|
||||
for (const theme of ["light", "dark"]) test(`Server rail: keyboard selection across pages and target sizes/${theme}`, async ({ page }) => {
|
||||
await page.addInitScript(value => localStorage.setItem("rl-theme", value), theme)
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", route => {
|
||||
const path = new URL(route.request().url()).pathname
|
||||
return route.fulfill({ json: path === "/api/auth/config" ? { required: false }
|
||||
: path === "/api/servers" ? Array.from({ length: 61 }, (_, i) => ({
|
||||
id: i + 1, name: `Роутер ${String(i + 1).padStart(3, "0")}`, host: `192.0.2.${i + 1}`,
|
||||
enabled: true, status: "online", type: "home-router", country: "RU",
|
||||
})) : path === "/api/gre/tunnels" ? { tunnels: [], failures: [] } : {} })
|
||||
})
|
||||
await page.goto("/gre")
|
||||
const rail = page.getByRole("listbox", { name: "Серверы", exact: true })
|
||||
const all = rail.getByRole("option", { name: /Все серверы/ })
|
||||
await expect(all).toHaveAttribute("aria-selected", "true")
|
||||
await all.focus()
|
||||
await page.keyboard.press("ArrowDown")
|
||||
const first = rail.getByRole("option", { name: /Роутер 001/ })
|
||||
await expect(first).toBeFocused()
|
||||
await expect(first).toHaveAttribute("aria-selected", "true")
|
||||
await expect(page).toHaveURL(/server=1(?:&|$)/)
|
||||
await page.keyboard.press("End")
|
||||
const last = rail.getByRole("option", { name: /Роутер 061/ })
|
||||
await expect(last).toBeFocused()
|
||||
await expect(last).toHaveAttribute("aria-selected", "true")
|
||||
await expect(last).toBeInViewport()
|
||||
await expect(page).toHaveURL(/server=61(?:&|$)/)
|
||||
await page.keyboard.press("ArrowUp")
|
||||
await expect(rail.getByRole("option", { name: /Роутер 060/ })).toBeFocused()
|
||||
await page.keyboard.press("Home")
|
||||
await expect(all).toBeFocused()
|
||||
await expect(all).toHaveAttribute("aria-selected", "true")
|
||||
await page.keyboard.press("Tab")
|
||||
await expect.poll(() => rail.evaluate(el => el.contains(document.activeElement))).toBe(false)
|
||||
await page.addScriptTag({ content: axe.source })
|
||||
const violations = await page.evaluate(async () => {
|
||||
const engine = (window as unknown as { axe: typeof axe }).axe
|
||||
return (await engine.run(document.querySelector('[role="listbox"][aria-label="Серверы"]')!, {
|
||||
runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"] },
|
||||
})).violations.map(v => ({ id: v.id, targets: v.nodes.map(n => n.target) }))
|
||||
})
|
||||
expect(violations).toEqual([])
|
||||
})
|
||||
Reference in new issue
Block a user