fix(network): исправить keepalive GRE и каталог IPsec
This commit is contained in:
1 parent
3f3f06f147
commit
727e35f985
28 files changed
+1722
-37
No files matched your search
@@ -357,6 +357,10 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
setFormError("Для IPsec нужен PSK не короче 8 символов")
|
||||
return
|
||||
}
|
||||
if (![tForm.keepaliveInterval, tForm.keepaliveRetries].every(value => Number.isInteger(value) && value >= 1 && value <= 4294967295)) {
|
||||
setFormError("Keepalive и число попыток: целые значения от 1 до 4294967295. Ноль не поддерживается этим API.")
|
||||
return
|
||||
}
|
||||
if (!beginOperation(tForm.serverId)) { setFormError("Устройство не готово к изменению GRE; повторите опрос"); return }
|
||||
formLock.current = true
|
||||
try {
|
||||
@@ -885,11 +889,11 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
<FormField label="MTU" hint="По умолч. 1476">
|
||||
<Input aria-label="MTU" type="number" className="font-mono" value={tForm.mtu} onChange={(e) => setT("mtu", Number(e.target.value))} />
|
||||
</FormField>
|
||||
<FormField label="Keepalive, с" hint="0 = откл.">
|
||||
<Input aria-label="Keepalive, с" type="number" className="font-mono" value={tForm.keepaliveInterval} onChange={(e) => setT("keepaliveInterval", Number(e.target.value))} />
|
||||
<FormField label="Keepalive, с" hint="Целое число, минимум 1 с">
|
||||
<Input aria-label="Keepalive, с" type="number" min={1} max={4294967295} step={1} className="font-mono" value={tForm.keepaliveInterval} onChange={(e) => setT("keepaliveInterval", Number(e.target.value))} />
|
||||
</FormField>
|
||||
<FormField label="Попытки">
|
||||
<Input aria-label="Попытки" type="number" className="font-mono" value={tForm.keepaliveRetries} onChange={(e) => setT("keepaliveRetries", Number(e.target.value))} />
|
||||
<Input aria-label="Попытки" type="number" min={1} max={4294967295} step={1} className="font-mono" value={tForm.keepaliveRetries} onChange={(e) => setT("keepaliveRetries", Number(e.target.value))} />
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="DSCP">
|
||||
|
||||
@@ -125,7 +125,7 @@ interface ApiGreTunnelRow {
|
||||
localInnerIp: string
|
||||
remoteInnerIp: string
|
||||
poolId: string
|
||||
ipsec: null
|
||||
ipsec: { secret: "" } | null
|
||||
mtu: number
|
||||
keepaliveInterval: number
|
||||
keepaliveRetries: number
|
||||
@@ -147,7 +147,7 @@ function apiGreToGreTunnel(t: ApiGreTunnelRow): GreTunnel {
|
||||
localInnerIp: t.localInnerIp,
|
||||
remoteInnerIp: t.remoteInnerIp,
|
||||
poolId: t.poolId || "live",
|
||||
ipsec: null,
|
||||
ipsec: t.ipsec,
|
||||
mtu: t.mtu,
|
||||
keepaliveInterval: t.keepaliveInterval,
|
||||
keepaliveRetries: t.keepaliveRetries,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"test:scheduler": "tsx src/services/scheduler-running.test.ts && tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts",
|
||||
"test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts",
|
||||
"test:ipsec": "tsx src/services/ipsec-config.test.ts && tsx src/services/ipsec-state.test.ts && tsx src/routes/ipsec-history.test.ts && tsx src/services/ipsec-restore.test.ts && tsx src/services/ipsec-operation.test.ts && tsx src/routes/ipsec-mutation.test.ts && tsx src/routes/ipsec-nat.test.ts && tsx src/routes/ipsec-group.test.ts",
|
||||
"test:live-maps": "tsx src/services/gre-read.test.ts && tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/routes/vxlan.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
|
||||
"test:live-maps": "tsx src/services/gre-keepalive.test.ts && tsx src/services/gre-read.test.ts && tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/routes/vxlan.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
|
||||
"test": "node scripts/test.mjs",
|
||||
"test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/routes/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts",
|
||||
"test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import {createServer} from 'node:http'
|
||||
import {randomBytes} from 'node:crypto'
|
||||
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
|
||||
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
|
||||
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own210.invalid';process.env.AUTH_PORTAL_URL='https://own210.invalid'
|
||||
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
|
||||
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
|
||||
|
||||
const rows=[],writes=[],cases=[];let app,serverId,nativeCalls=0;
|
||||
const native=createServer(async(req,res)=>{nativeCalls++;let text='';for await(const chunk of req)text+=chunk;const body=text?JSON.parse(text):{};res.setHeader('content-type','application/json');if(req.method==='GET'){res.end(JSON.stringify(req.url.endsWith('/interface/gre')?rows:[]));return}writes.push({method:req.method,path:req.url,body});if(req.method==='PUT'){const row={...body,'.id':'*'+(rows.length+1),running:'false'};rows.push(row);res.end(JSON.stringify(row));return}if(req.method==='PATCH'){const id=decodeURIComponent(req.url.split('/').pop()),row=rows.find(r=>r['.id']===id);assert(row);Object.assign(row,body);res.end(JSON.stringify(row));return}res.statusCode=500;res.end('{}')});await new Promise(r=>native.listen(0,'127.0.0.1',r));
|
||||
const report={date:'2026-10-13',block:'12.5.210',database,cases,scope:'Production compiled GRE create/patch using isolated PostgreSQL and native HTTP fixture',passed:false};
|
||||
try{await initDatabase();serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('own210-native','127.0.0.1',$1,false,true,'fixture','fixture') RETURNING id",[native.address().port])).rows[0].id);app=await buildApp({logger:false,startScheduler:false});await app.ready();const token=app.jwt.sign({sub:'own210-writer',apps:['mm'],permissions:['mm:network:write'],iss:process.env.AUTH_ISSUER},{expiresIn:'5m'}),headers={authorization:'Bearer '+token};
|
||||
for(const field of ['keepaliveInterval','keepaliveRetries'])for(const value of [0,-1,1.5,4294967296]){const before=nativeCalls;const r=await app.inject({method:'POST',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),name:'invalid',remoteAddress:'192.0.2.2',[field]:value}});assert.equal(r.statusCode,400);assert.equal(nativeCalls,before);cases.push({field,value,status:400,nativeCalls:0})}
|
||||
for(const params of [{},{keepaliveInterval:60},{keepaliveRetries:3},{keepaliveInterval:90,keepaliveRetries:7}]){const name='own210-'+rows.length;const r=await app.inject({method:'POST',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),name,remoteAddress:'192.0.2.2',enabled:false,...params}});assert.equal(r.statusCode,201);const expected=(params.keepaliveInterval??10)+'s,'+(params.keepaliveRetries??10);assert.equal(rows.at(-1).keepalive,expected);cases.push({operation:'create',params,keepalive:expected,status:201})}
|
||||
rows[0].keepalive='1m,3';const patch=await app.inject({method:'PATCH',url:'/api/gre/tunnels',headers,payload:{serverId:String(serverId),rosId:rows[0]['.id'],comment:'own210 updated'}});assert.equal(patch.statusCode,200);assert.equal(rows[0].keepalive,'1m,3');assert.equal(rows[0].comment,'own210 updated');cases.push({operation:'patch-other-field',nativeKeepalivePreserved:'1m,3',status:200});const read=await app.inject({method:'GET',url:'/api/gre/tunnels?serverId='+serverId,headers});assert.equal(read.statusCode,200);assert.equal(read.json().tunnels.find(t=>t.name===rows[0].name).keepaliveInterval,60);cases.push({operation:'read-minutes',seconds:60,status:200});report.passed=true;report.nativeWrites=writes.length;
|
||||
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre210-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();native.closeAllConnections();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/app/backend/acceptance/gre210-keepalive-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1;
|
||||
@@ -5,6 +5,7 @@ import { db } from "../db/index.js"
|
||||
import { filterRules, recursiveRoutes, servers } from "../db/schema.js"
|
||||
import { MikrotikClient } from "../services/mikrotik.js"
|
||||
import { pollGreCatalog } from "../services/gre-catalog-poll.js"
|
||||
import { parseKeepalive } from "../services/gre-live.js"
|
||||
import { parseDbServerId } from "../utils/server-id.js"
|
||||
import { appendEvent } from "../modules/events/service/events-service.js"
|
||||
import { managedComment } from "../managed-markers.js"
|
||||
@@ -74,17 +75,6 @@ interface ApiFilterRule {
|
||||
description: string
|
||||
}
|
||||
|
||||
function parseKeepalive(value: string | undefined): { interval: number; retries: number } {
|
||||
if (!value || value.toLowerCase() === "none") return { interval: 0, retries: 0 }
|
||||
const [intervalRaw, retriesRaw] = value.split(",")
|
||||
const interval = Number.parseInt((intervalRaw ?? "").trim(), 10)
|
||||
const retries = Number.parseInt((retriesRaw ?? "").trim(), 10)
|
||||
return {
|
||||
interval: Number.isFinite(interval) ? interval : 0,
|
||||
retries: Number.isFinite(retries) ? retries : 0,
|
||||
}
|
||||
}
|
||||
|
||||
function parseDscp(value: string | undefined): "inherit" | number {
|
||||
if (!value || value === "inherit") return "inherit"
|
||||
const n = Number.parseInt(value, 10)
|
||||
|
||||
@@ -43,8 +43,8 @@ const TunnelWriteSchema = z.object({
|
||||
comment: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
mtu: z.number().optional(),
|
||||
keepaliveInterval: z.number().optional(),
|
||||
keepaliveRetries: z.number().optional(),
|
||||
keepaliveInterval: z.number().int().min(1).max(4294967295).optional(),
|
||||
keepaliveRetries: z.number().int().min(1).max(4294967295).optional(),
|
||||
dscp: z.union([z.literal("inherit"), z.number(), z.string()]).optional(),
|
||||
clampTcpMss: z.boolean().optional(),
|
||||
allowFastPath: z.boolean().optional(),
|
||||
@@ -84,9 +84,7 @@ function tunnelFromBody(body: z.infer<typeof TunnelWriteSchema> & { keepaliveInt
|
||||
: typeof body.dscp === "number"
|
||||
? String(body.dscp)
|
||||
: body.dscp
|
||||
const keepalive = body.keepaliveInterval === undefined && body.keepaliveRetries === undefined
|
||||
? undefined
|
||||
: formatKeepalive(body.keepaliveInterval ?? 0, body.keepaliveRetries ?? 10)
|
||||
const keepalive = formatKeepalive(body.keepaliveInterval ?? 10, body.keepaliveRetries ?? 10)
|
||||
return canonicalGreSnapshot({
|
||||
tunnels: [{
|
||||
name: body.name,
|
||||
@@ -97,7 +95,7 @@ function tunnelFromBody(body: z.infer<typeof TunnelWriteSchema> & { keepaliveInt
|
||||
comment: body.remoteInnerIp === undefined ? body.comment ?? "" : writeGreComment(body.comment ?? "", body.remoteInnerIp),
|
||||
disabled: body.enabled === false,
|
||||
mtu: body.mtu ?? 1476,
|
||||
keepalive: keepalive ?? "0",
|
||||
keepalive,
|
||||
dscp,
|
||||
clampTcpMss: body.clampTcpMss,
|
||||
allowFastPath: body.allowFastPath,
|
||||
@@ -173,6 +171,10 @@ const greRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
allowFastPath: body.allowFastPath ?? live.allowFastPath,
|
||||
ipsecSecret: body.ipsecSecret ?? live.ipsecSecret,
|
||||
})
|
||||
// A patch to another field must not reformat an observed native timer.
|
||||
if (body.keepaliveInterval === undefined && body.keepaliveRetries === undefined) {
|
||||
merged.keepalive = original.keepalive
|
||||
}
|
||||
merged.innerAddresses = original.innerAddresses?.map(a => ({ ...a }))
|
||||
if (body.localInnerIp !== undefined && body.localInnerIp !== original.localInnerIp) {
|
||||
merged.innerAddresses = (merged.innerAddresses ?? []).filter(a => a.address !== original.localInnerIp)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { parseKeepalive } from "./gre-live.js"
|
||||
|
||||
for (const [native, seconds] of [["1m,3", 60], ["1m30s,3", 90], ["00:01:00,3", 60], ["1h2m3s,3", 3723], ["10s,3", 10], ["10,3", 10]] as const) {
|
||||
assert.deepEqual(parseKeepalive(native), { interval: seconds, retries: 3 })
|
||||
}
|
||||
assert.deepEqual(parseKeepalive("invalid,3"), { interval: 0, retries: 3 })
|
||||
assert.deepEqual(parseKeepalive("10s,3garbage"), { interval: 10, retries: 0 })
|
||||
console.log("GRE keepalive: native units and clock parsed without prefix truncation")
|
||||
@@ -3,6 +3,7 @@ import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
import { readGreComment } from "./gre-comment.js"
|
||||
import { observedDurationMs, observedInteger } from "./ospf-observed-values.js"
|
||||
import {
|
||||
canonicalGreSnapshot,
|
||||
greAddressPath,
|
||||
@@ -69,11 +70,14 @@ export interface LiveGreTunnel {
|
||||
export function parseKeepalive(value: string | undefined): { interval: number; retries: number } {
|
||||
if (!value || value.toLowerCase() === "none") return { interval: 0, retries: 0 }
|
||||
const [intervalRaw, retriesRaw] = value.split(",")
|
||||
const interval = Number.parseInt((intervalRaw ?? "").trim(), 10)
|
||||
const retries = Number.parseInt((retriesRaw ?? "").trim(), 10)
|
||||
const intervalText = intervalRaw?.trim()
|
||||
const interval = intervalText && /^\d+$/.test(intervalText)
|
||||
? observedInteger(intervalText)
|
||||
: (observedDurationMs(intervalText) ?? 0) / 1000
|
||||
const retries = observedInteger(retriesRaw)
|
||||
return {
|
||||
interval: Number.isFinite(interval) ? interval : 0,
|
||||
retries: Number.isFinite(retries) ? retries : 0,
|
||||
interval: interval ?? 0,
|
||||
retries: retries ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# UX/UI: протокол внешней приёмки 12.5
|
||||
|
||||
Актуальное продолжение 2026-10-13: блок 209 завершён. GRE PSK скрыт от читателя; каталог Filters сохраняет признак настроенного IPsec без ключа. Пройдены 12 SQL/API-проверок, профиль live-maps, 4 E2E и 4 live UI-сценария. Два собственных отключённых GRE на CHR созданы и удалены. Backend/frontend: ux-gre209, 473/527 исходных файлов и 1048 compiled artifacts совпали. Следующий блок 210 — keepalive при создании GRE без явных параметров. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 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).
|
||||
|
||||
@@ -2539,3 +2539,20 @@ Backend/frontend ux-gre209 развёрнуты; 473/527 source files и 1048 co
|
||||
|
||||
Следующий блок 210: исправить создание GRE с опущенным keepalive, проверить фактический RouterOS ответ и прежнее редактирование. Этап 12.5 открыт; внешние ограничения в актуальной матрице сохраняются.
|
||||
|
||||
|
||||
## 12.5.210 — GRE keepalive и совместимость публичного каталога (2026-10-13)
|
||||
|
||||
На обоих CHR собственные отключённые интерфейсы подтвердили: native REST отклоняет 0, none, пустую строку, disabled, no и ноль попыток. Дробные интервалы принимаются, но сокращаются RouterOS до целых секунд. Поэтому форма и write schema требуют целые значения 1..4294967295 вместо обещания «0 = откл.». Неверный ввод сохраняет форму и не отправляет запрос. Официальный default 10s,10: [MikroTik GRE](https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE).
|
||||
|
||||
Опущенные параметры создания теперь дают 10s,10; один заданный параметр сохраняется, второй получает default. Основной GET и каталог Filters используют общий разбор native единиц/clock: 1m = 60с, 1m30s = 90с. PATCH без параметров keepalive сохраняет исходную строку snapshot, не преобразует таймер при изменении комментария/PSK. Legacy snapshots не переписаны; произвольные legacy/unknown timers и их экспорт не приняты этим блоком.
|
||||
|
||||
Аудит потребителей выявил несовместимость 209: shared schema Filters допускала только ipsec:null. Теперь она принимает {secret:""} или null, отвергает реальный ключ. Карта сохраняет configured IPsec; Filters и диагностика используют обновлённую схему. ReUI primitives и theme не менялись.
|
||||
|
||||
Проверки: 24 direct native syntax cases на отключённых собственных GRE, с очисткой; production SQL/API14 на собственной ограниченной DB/role, включая 8 invalid cases до native requests, четыре создания, сохранение native timer и чтение минут. SQL DB/role/container удалены. Профиль live-maps, schema unit, 5 network-map units, TypeScript/backend build/ESLint прошли. 13 уникальных E2E: GRE формы/PSK/валидация9 и Filters gateway/retry4. Копии fixtures имеют configured IPsec; прежние повторные действия сохранены.
|
||||
|
||||
Runtime ux-gre210: backend healthy, source475/527 и compiled1048 matched. На каждом CHR приложение создало отключённый GRE без keepalive и с явным allowFastPath=false для PSK; native default10s,10 подтверждён. После native 1m,3 GET показывает60с, PATCH комментария сохраняет1m,3, explicit0 даёт400 без изменения native. Два UI GRE и два UI Filters на1440/320 проверили форму, отсутствие запроса при invalid input и доступность собственного GRE с IPsec в каталоге шлюзов. Оба интерфейса удалены, отсутствие проверено. Пароли не менялись; это не data-plane test.
|
||||
|
||||
Два первых create с PSK и опущенным allowFastPath получили502: текущий API default FastPath несовместим с PSK. Это отдельное обнаруженное ограничение, не скрытое успешным повтором; дальнейшая проверка задала false явно. Следующий приоритет211 — подтверждённый обход auth через encoded API prefix. После него исправить FastPath/PSK default и экспорт недопустимого/неизвестного keepalive.
|
||||
|
||||
Протоколы: [native syntax](ux-ui/acceptance/gre210-native-range-2026-10-13.json), [дополнительный синтаксис](ux-ui/acceptance/gre210-native-range-extra-2026-10-13.json), [SQL/API14](ux-ui/acceptance/gre210-keepalive-result.json), [профили](ux-ui/acceptance/gre210-profile-2026-10-13.json), [E2E13](ux-ui/acceptance/gre210-e2e-2026-10-13.json), [native/UI/cleanup](ux-ui/acceptance/gre210-native-live-2026-10-13.json), [source](ux-ui/acceptance/source210-result.json), [compiled](ux-ui/acceptance/compiled210-result.json), [deployment](ux-ui/acceptance/gre210-deployment-2026-10-13.json).
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# План реализации UX/UI по пятичасовым окнам
|
||||
|
||||
Актуальное продолжение 2026-10-13: блок 209 завершён. GRE PSK скрыт от читателя; каталог Filters сохраняет признак настроенного IPsec без ключа. Пройдены 12 SQL/API-проверок, профиль live-maps, 4 E2E и 4 live UI-сценария. Два собственных отключённых GRE на CHR созданы и удалены. Backend/frontend: ux-gre209, 473/527 исходных файлов и 1048 compiled artifacts совпали. Следующий блок 210 — keepalive при создании GRE без явных параметров. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
|
||||
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-13. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
|
||||
+17
-1
@@ -1,6 +1,6 @@
|
||||
# UX/UI: журнал реализации
|
||||
|
||||
Актуальное продолжение 2026-10-13: блок 209 завершён. GRE PSK скрыт от читателя; каталог Filters сохраняет признак настроенного IPsec без ключа. Пройдены 12 SQL/API-проверок, профиль live-maps, 4 E2E и 4 live UI-сценария. Два собственных отключённых GRE на CHR созданы и удалены. Backend/frontend: ux-gre209, 473/527 исходных файлов и 1048 compiled artifacts совпали. Следующий блок 210 — keepalive при создании GRE без явных параметров. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
|
||||
|
||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
||||
|
||||
@@ -38,6 +38,22 @@
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–196 и ограничения ниже. Runtime backend/frontend ux-peak196. |
|
||||
|
||||
## 12.5.210 — GRE keepalive и совместимость публичного каталога (2026-10-13)
|
||||
|
||||
На обоих CHR собственные отключённые интерфейсы подтвердили: native REST отклоняет 0, none, пустую строку, disabled, no и ноль попыток. Дробные интервалы принимаются, но сокращаются RouterOS до целых секунд. Поэтому форма и write schema требуют целые значения 1..4294967295 вместо обещания «0 = откл.». Неверный ввод сохраняет форму и не отправляет запрос. Официальный default 10s,10: [MikroTik GRE](https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE).
|
||||
|
||||
Опущенные параметры создания теперь дают 10s,10; один заданный параметр сохраняется, второй получает default. Основной GET и каталог Filters используют общий разбор native единиц/clock: 1m = 60с, 1m30s = 90с. PATCH без параметров keepalive сохраняет исходную строку snapshot, не преобразует таймер при изменении комментария/PSK. Legacy snapshots не переписаны; произвольные legacy/unknown timers и их экспорт не приняты этим блоком.
|
||||
|
||||
Аудит потребителей выявил несовместимость 209: shared schema Filters допускала только ipsec:null. Теперь она принимает {secret:""} или null, отвергает реальный ключ. Карта сохраняет configured IPsec; Filters и диагностика используют обновлённую схему. ReUI primitives и theme не менялись.
|
||||
|
||||
Проверки: 24 direct native syntax cases на отключённых собственных GRE, с очисткой; production SQL/API14 на собственной ограниченной DB/role, включая 8 invalid cases до native requests, четыре создания, сохранение native timer и чтение минут. SQL DB/role/container удалены. Профиль live-maps, schema unit, 5 network-map units, TypeScript/backend build/ESLint прошли. 13 уникальных E2E: GRE формы/PSK/валидация9 и Filters gateway/retry4. Копии fixtures имеют configured IPsec; прежние повторные действия сохранены.
|
||||
|
||||
Runtime ux-gre210: backend healthy, source475/527 и compiled1048 matched. На каждом CHR приложение создало отключённый GRE без keepalive и с явным allowFastPath=false для PSK; native default10s,10 подтверждён. После native 1m,3 GET показывает60с, PATCH комментария сохраняет1m,3, explicit0 даёт400 без изменения native. Два UI GRE и два UI Filters на1440/320 проверили форму, отсутствие запроса при invalid input и доступность собственного GRE с IPsec в каталоге шлюзов. Оба интерфейса удалены, отсутствие проверено. Пароли не менялись; это не data-plane test.
|
||||
|
||||
Два первых create с PSK и опущенным allowFastPath получили502: текущий API default FastPath несовместим с PSK. Это отдельное обнаруженное ограничение, не скрытое успешным повтором; дальнейшая проверка задала false явно. Следующий приоритет211 — подтверждённый обход auth через encoded API prefix. После него исправить FastPath/PSK default и экспорт недопустимого/неизвестного keepalive.
|
||||
|
||||
Протоколы: [native syntax](ux-ui/acceptance/gre210-native-range-2026-10-13.json), [дополнительный синтаксис](ux-ui/acceptance/gre210-native-range-extra-2026-10-13.json), [SQL/API14](ux-ui/acceptance/gre210-keepalive-result.json), [профили](ux-ui/acceptance/gre210-profile-2026-10-13.json), [E2E13](ux-ui/acceptance/gre210-e2e-2026-10-13.json), [native/UI/cleanup](ux-ui/acceptance/gre210-native-live-2026-10-13.json), [source](ux-ui/acceptance/source210-result.json), [compiled](ux-ui/acceptance/compiled210-result.json), [deployment](ux-ui/acceptance/gre210-deployment-2026-10-13.json).
|
||||
|
||||
## 12.5.209 — доступ к GRE PSK и достоверность каталога IPsec (2026-10-13)
|
||||
|
||||
В production baseline читатель получал PSK через GET /gre/tunnels. Каталог /filters/gre-tunnels ключ не раскрывал, но всегда возвращал ipsec:null, скрывая сам факт настройки IPsec. Основной GET теперь копирует публичный DTO и очищает secret при отсутствии mm:network:write/admin; алгоритмы и наличие объекта IPsec сохраняются. Internal snapshot и право редактирования для writer/admin не меняются. is_admin без permission не даёт доступ к ключу. Legacy AUTH_REQUIRED=false сохранён. Общий каталог Filters возвращает только признак IPsec и пустой secret для всех ролей.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Оставшаяся внешняя приёмка — 2026-10-13
|
||||
|
||||
Актуальное продолжение 2026-10-13: блок 209 завершён. GRE PSK скрыт от читателя; каталог Filters сохраняет признак настроенного IPsec без ключа. Пройдены 12 SQL/API-проверок, профиль live-maps, 4 E2E и 4 live UI-сценария. Два собственных отключённых GRE на CHR созданы и удалены. Backend/frontend: ux-gre209, 473/527 исходных файлов и 1048 compiled artifacts совпали. Следующий блок 210 — keepalive при создании GRE без явных параметров. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
|
||||
|
||||
Это актуальная матрица после 12.5.209. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
Это актуальная матрица после 12.5.210. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend/frontend `ux-gre209`; 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/frontend `ux-gre210`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
|
||||
| Раздел | Подтверждено | Осталось / условие завершения |
|
||||
| --- | --- | --- |
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
## Следующая последовательность — 2026-10-13
|
||||
|
||||
1. **210 — GRE keepalive:** исправить значение по умолчанию создания, которое RouterOS отклоняет; проверить отсутствие неожиданных изменений при PATCH.
|
||||
1. **211 — canonical auth path:** закрыть обход JWT/RBAC через encoded API prefix; actual anonymous baseline200 сохранён без raw body/секретов. После него — FastPath/PSK default и экспорт неизвестного keepalive.
|
||||
2. **Профильный контроль:** для новых изменений выполнять затронутые SQL/API/UI проверки. Общая регрессия192 выполнена; не повторять её без изменений общей основы или нового основания.
|
||||
3. **Внешние зависимости:** Portal issuer/login/revoke, Telegram bot/chat/разрешённое сообщение, клиентские ОС, независимые WAN/public IPv6 и scale/multi-instance — отдельные группы по таблице выше. Ограничения текущего стенда не превращать в обещание приёмки всех конфигураций.
|
||||
|
||||
@@ -1457,3 +1457,20 @@ Backend/frontend ux-gre209 развёрнуты; 473/527 source files и 1048 co
|
||||
|
||||
Следующий блок 210: исправить создание GRE с опущенным keepalive, проверить фактический RouterOS ответ и прежнее редактирование. Этап 12.5 открыт; внешние ограничения в актуальной матрице сохраняются.
|
||||
|
||||
|
||||
## 12.5.210 — GRE keepalive и совместимость публичного каталога (2026-10-13)
|
||||
|
||||
На обоих CHR собственные отключённые интерфейсы подтвердили: native REST отклоняет 0, none, пустую строку, disabled, no и ноль попыток. Дробные интервалы принимаются, но сокращаются RouterOS до целых секунд. Поэтому форма и write schema требуют целые значения 1..4294967295 вместо обещания «0 = откл.». Неверный ввод сохраняет форму и не отправляет запрос. Официальный default 10s,10: [MikroTik GRE](https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE).
|
||||
|
||||
Опущенные параметры создания теперь дают 10s,10; один заданный параметр сохраняется, второй получает default. Основной GET и каталог Filters используют общий разбор native единиц/clock: 1m = 60с, 1m30s = 90с. PATCH без параметров keepalive сохраняет исходную строку snapshot, не преобразует таймер при изменении комментария/PSK. Legacy snapshots не переписаны; произвольные legacy/unknown timers и их экспорт не приняты этим блоком.
|
||||
|
||||
Аудит потребителей выявил несовместимость 209: shared schema Filters допускала только ipsec:null. Теперь она принимает {secret:""} или null, отвергает реальный ключ. Карта сохраняет configured IPsec; Filters и диагностика используют обновлённую схему. ReUI primitives и theme не менялись.
|
||||
|
||||
Проверки: 24 direct native syntax cases на отключённых собственных GRE, с очисткой; production SQL/API14 на собственной ограниченной DB/role, включая 8 invalid cases до native requests, четыре создания, сохранение native timer и чтение минут. SQL DB/role/container удалены. Профиль live-maps, schema unit, 5 network-map units, TypeScript/backend build/ESLint прошли. 13 уникальных E2E: GRE формы/PSK/валидация9 и Filters gateway/retry4. Копии fixtures имеют configured IPsec; прежние повторные действия сохранены.
|
||||
|
||||
Runtime ux-gre210: backend healthy, source475/527 и compiled1048 matched. На каждом CHR приложение создало отключённый GRE без keepalive и с явным allowFastPath=false для PSK; native default10s,10 подтверждён. После native 1m,3 GET показывает60с, PATCH комментария сохраняет1m,3, explicit0 даёт400 без изменения native. Два UI GRE и два UI Filters на1440/320 проверили форму, отсутствие запроса при invalid input и доступность собственного GRE с IPsec в каталоге шлюзов. Оба интерфейса удалены, отсутствие проверено. Пароли не менялись; это не data-plane test.
|
||||
|
||||
Два первых create с PSK и опущенным allowFastPath получили502: текущий API default FastPath несовместим с PSK. Это отдельное обнаруженное ограничение, не скрытое успешным повтором; дальнейшая проверка задала false явно. Следующий приоритет211 — подтверждённый обход auth через encoded API prefix. После него исправить FastPath/PSK default и экспорт недопустимого/неизвестного keepalive.
|
||||
|
||||
Протоколы: [native syntax](ux-ui/acceptance/gre210-native-range-2026-10-13.json), [дополнительный синтаксис](ux-ui/acceptance/gre210-native-range-extra-2026-10-13.json), [SQL/API14](ux-ui/acceptance/gre210-keepalive-result.json), [профили](ux-ui/acceptance/gre210-profile-2026-10-13.json), [E2E13](ux-ui/acceptance/gre210-e2e-2026-10-13.json), [native/UI/cleanup](ux-ui/acceptance/gre210-native-live-2026-10-13.json), [source](ux-ui/acceptance/source210-result.json), [compiled](ux-ui/acceptance/compiled210-result.json), [deployment](ux-ui/acceptance/gre210-deployment-2026-10-13.json).
|
||||
|
||||
+18
-1
@@ -1,6 +1,6 @@
|
||||
# Этап 12: итоговая приёмка UX/UI
|
||||
|
||||
Актуальное продолжение 2026-10-13: блок 209 завершён. GRE PSK скрыт от читателя; каталог Filters сохраняет признак настроенного IPsec без ключа. Пройдены 12 SQL/API-проверок, профиль live-maps, 4 E2E и 4 live UI-сценария. Два собственных отключённых GRE на CHR созданы и удалены. Backend/frontend: ux-gre209, 473/527 исходных файлов и 1048 compiled artifacts совпали. Следующий блок 210 — keepalive при создании GRE без явных параметров. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
|
||||
|
||||
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
|
||||
|
||||
@@ -3846,3 +3846,20 @@ Backend/frontend ux-gre209 развёрнуты; 473/527 source files и 1048 co
|
||||
|
||||
Следующий блок 210: исправить создание GRE с опущенным keepalive, проверить фактический RouterOS ответ и прежнее редактирование. Этап 12.5 открыт; внешние ограничения в актуальной матрице сохраняются.
|
||||
|
||||
|
||||
## 12.5.210 — GRE keepalive и совместимость публичного каталога (2026-10-13)
|
||||
|
||||
На обоих CHR собственные отключённые интерфейсы подтвердили: native REST отклоняет 0, none, пустую строку, disabled, no и ноль попыток. Дробные интервалы принимаются, но сокращаются RouterOS до целых секунд. Поэтому форма и write schema требуют целые значения 1..4294967295 вместо обещания «0 = откл.». Неверный ввод сохраняет форму и не отправляет запрос. Официальный default 10s,10: [MikroTik GRE](https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE).
|
||||
|
||||
Опущенные параметры создания теперь дают 10s,10; один заданный параметр сохраняется, второй получает default. Основной GET и каталог Filters используют общий разбор native единиц/clock: 1m = 60с, 1m30s = 90с. PATCH без параметров keepalive сохраняет исходную строку snapshot, не преобразует таймер при изменении комментария/PSK. Legacy snapshots не переписаны; произвольные legacy/unknown timers и их экспорт не приняты этим блоком.
|
||||
|
||||
Аудит потребителей выявил несовместимость 209: shared schema Filters допускала только ipsec:null. Теперь она принимает {secret:""} или null, отвергает реальный ключ. Карта сохраняет configured IPsec; Filters и диагностика используют обновлённую схему. ReUI primitives и theme не менялись.
|
||||
|
||||
Проверки: 24 direct native syntax cases на отключённых собственных GRE, с очисткой; production SQL/API14 на собственной ограниченной DB/role, включая 8 invalid cases до native requests, четыре создания, сохранение native timer и чтение минут. SQL DB/role/container удалены. Профиль live-maps, schema unit, 5 network-map units, TypeScript/backend build/ESLint прошли. 13 уникальных E2E: GRE формы/PSK/валидация9 и Filters gateway/retry4. Копии fixtures имеют configured IPsec; прежние повторные действия сохранены.
|
||||
|
||||
Runtime ux-gre210: backend healthy, source475/527 и compiled1048 matched. На каждом CHR приложение создало отключённый GRE без keepalive и с явным allowFastPath=false для PSK; native default10s,10 подтверждён. После native 1m,3 GET показывает60с, PATCH комментария сохраняет1m,3, explicit0 даёт400 без изменения native. Два UI GRE и два UI Filters на1440/320 проверили форму, отсутствие запроса при invalid input и доступность собственного GRE с IPsec в каталоге шлюзов. Оба интерфейса удалены, отсутствие проверено. Пароли не менялись; это не data-plane test.
|
||||
|
||||
Два первых create с PSK и опущенным allowFastPath получили502: текущий API default FastPath несовместим с PSK. Это отдельное обнаруженное ограничение, не скрытое успешным повтором; дальнейшая проверка задала false явно. Следующий приоритет211 — подтверждённый обход auth через encoded API prefix. После него исправить FastPath/PSK default и экспорт недопустимого/неизвестного keepalive.
|
||||
|
||||
Протоколы: [native syntax](ux-ui/acceptance/gre210-native-range-2026-10-13.json), [дополнительный синтаксис](ux-ui/acceptance/gre210-native-range-extra-2026-10-13.json), [SQL/API14](ux-ui/acceptance/gre210-keepalive-result.json), [профили](ux-ui/acceptance/gre210-profile-2026-10-13.json), [E2E13](ux-ui/acceptance/gre210-e2e-2026-10-13.json), [native/UI/cleanup](ux-ui/acceptance/gre210-native-live-2026-10-13.json), [source](ux-ui/acceptance/source210-result.json), [compiled](ux-ui/acceptance/compiled210-result.json), [deployment](ux-ui/acceptance/gre210-deployment-2026-10-13.json).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Тестовый сервер UX/UI — 2026-10-10
|
||||
|
||||
Актуальное продолжение 2026-10-13: блок 209 завершён. GRE PSK скрыт от читателя; каталог Filters сохраняет признак настроенного IPsec без ключа. Пройдены 12 SQL/API-проверок, профиль live-maps, 4 E2E и 4 live UI-сценария. Два собственных отключённых GRE на CHR созданы и удалены. Backend/frontend: ux-gre209, 473/527 исходных файлов и 1048 compiled artifacts совпали. Следующий блок 210 — keepalive при создании GRE без явных параметров. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
|
||||
|
||||
| Параметр | Проверенное состояние |
|
||||
| --- | --- |
|
||||
@@ -2270,3 +2270,20 @@ Backend/frontend ux-gre209 развёрнуты; 473/527 source files и 1048 co
|
||||
|
||||
Следующий блок 210: исправить создание GRE с опущенным keepalive, проверить фактический RouterOS ответ и прежнее редактирование. Этап 12.5 открыт; внешние ограничения в актуальной матрице сохраняются.
|
||||
|
||||
|
||||
## 12.5.210 — GRE keepalive и совместимость публичного каталога (2026-10-13)
|
||||
|
||||
На обоих CHR собственные отключённые интерфейсы подтвердили: native REST отклоняет 0, none, пустую строку, disabled, no и ноль попыток. Дробные интервалы принимаются, но сокращаются RouterOS до целых секунд. Поэтому форма и write schema требуют целые значения 1..4294967295 вместо обещания «0 = откл.». Неверный ввод сохраняет форму и не отправляет запрос. Официальный default 10s,10: [MikroTik GRE](https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE).
|
||||
|
||||
Опущенные параметры создания теперь дают 10s,10; один заданный параметр сохраняется, второй получает default. Основной GET и каталог Filters используют общий разбор native единиц/clock: 1m = 60с, 1m30s = 90с. PATCH без параметров keepalive сохраняет исходную строку snapshot, не преобразует таймер при изменении комментария/PSK. Legacy snapshots не переписаны; произвольные legacy/unknown timers и их экспорт не приняты этим блоком.
|
||||
|
||||
Аудит потребителей выявил несовместимость 209: shared schema Filters допускала только ipsec:null. Теперь она принимает {secret:""} или null, отвергает реальный ключ. Карта сохраняет configured IPsec; Filters и диагностика используют обновлённую схему. ReUI primitives и theme не менялись.
|
||||
|
||||
Проверки: 24 direct native syntax cases на отключённых собственных GRE, с очисткой; production SQL/API14 на собственной ограниченной DB/role, включая 8 invalid cases до native requests, четыре создания, сохранение native timer и чтение минут. SQL DB/role/container удалены. Профиль live-maps, schema unit, 5 network-map units, TypeScript/backend build/ESLint прошли. 13 уникальных E2E: GRE формы/PSK/валидация9 и Filters gateway/retry4. Копии fixtures имеют configured IPsec; прежние повторные действия сохранены.
|
||||
|
||||
Runtime ux-gre210: backend healthy, source475/527 и compiled1048 matched. На каждом CHR приложение создало отключённый GRE без keepalive и с явным allowFastPath=false для PSK; native default10s,10 подтверждён. После native 1m,3 GET показывает60с, PATCH комментария сохраняет1m,3, explicit0 даёт400 без изменения native. Два UI GRE и два UI Filters на1440/320 проверили форму, отсутствие запроса при invalid input и доступность собственного GRE с IPsec в каталоге шлюзов. Оба интерфейса удалены, отсутствие проверено. Пароли не менялись; это не data-plane test.
|
||||
|
||||
Два первых create с PSK и опущенным allowFastPath получили502: текущий API default FastPath несовместим с PSK. Это отдельное обнаруженное ограничение, не скрытое успешным повтором; дальнейшая проверка задала false явно. Следующий приоритет211 — подтверждённый обход auth через encoded API prefix. После него исправить FastPath/PSK default и экспорт недопустимого/неизвестного keepalive.
|
||||
|
||||
Протоколы: [native syntax](ux-ui/acceptance/gre210-native-range-2026-10-13.json), [дополнительный синтаксис](ux-ui/acceptance/gre210-native-range-extra-2026-10-13.json), [SQL/API14](ux-ui/acceptance/gre210-keepalive-result.json), [профили](ux-ui/acceptance/gre210-profile-2026-10-13.json), [E2E13](ux-ui/acceptance/gre210-e2e-2026-10-13.json), [native/UI/cleanup](ux-ui/acceptance/gre210-native-live-2026-10-13.json), [source](ux-ui/acceptance/source210-result.json), [compiled](ux-ui/acceptance/compiled210-result.json), [deployment](ux-ui/acceptance/gre210-deployment-2026-10-13.json).
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.210",
|
||||
"runtime": "ux-gre210",
|
||||
"services": {
|
||||
"backend": {
|
||||
"image": "mmapp-test/backend:ux-gre210",
|
||||
"imageId": "sha256:9bce7d7505145a6479cf2920240abacc5e38aa1aafacfd7c69f1fcaaf3a900a7",
|
||||
"running": true,
|
||||
"health": "healthy"
|
||||
},
|
||||
"frontend": {
|
||||
"image": "mmapp-test/frontend:ux-gre210",
|
||||
"imageId": "sha256:b4b1280f729e89de83b30bd242d37bda831306720b3c78cb9eef6048c3e3178e",
|
||||
"running": true,
|
||||
"health": null
|
||||
}
|
||||
},
|
||||
"rollbackTags": {
|
||||
"MMAPP_IMAGE_TAG": "ux-gre209",
|
||||
"BACKEND_IMAGE_TAG": "ux-gre209"
|
||||
},
|
||||
"chrPasswordsChanged": false,
|
||||
"source": [
|
||||
{
|
||||
"kind": "backend",
|
||||
"matched": 475
|
||||
},
|
||||
{
|
||||
"kind": "frontend",
|
||||
"matched": 527
|
||||
}
|
||||
],
|
||||
"compiledMatched": 1048
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.210",
|
||||
"uniqueCases": 13,
|
||||
"groups": [
|
||||
{
|
||||
"scope": "GRE keepalive, PSK and form receipts",
|
||||
"stats": {
|
||||
"startTime": "2026-10-13T13:54:18.301Z",
|
||||
"duration": 22078.386000000002,
|
||||
"expected": 9,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "Filters gateway catalog masked-IPsec and original retry",
|
||||
"stats": {
|
||||
"startTime": "2026-10-13T13:59:12.012Z",
|
||||
"duration": 15935.284,
|
||||
"expected": 4,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"apiMocked": true
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.210",
|
||||
"database": "mm_accept_166_1791900297484",
|
||||
"cases": [
|
||||
{
|
||||
"field": "keepaliveInterval",
|
||||
"value": 0,
|
||||
"status": 400,
|
||||
"nativeCalls": 0
|
||||
},
|
||||
{
|
||||
"field": "keepaliveInterval",
|
||||
"value": -1,
|
||||
"status": 400,
|
||||
"nativeCalls": 0
|
||||
},
|
||||
{
|
||||
"field": "keepaliveInterval",
|
||||
"value": 1.5,
|
||||
"status": 400,
|
||||
"nativeCalls": 0
|
||||
},
|
||||
{
|
||||
"field": "keepaliveInterval",
|
||||
"value": 4294967296,
|
||||
"status": 400,
|
||||
"nativeCalls": 0
|
||||
},
|
||||
{
|
||||
"field": "keepaliveRetries",
|
||||
"value": 0,
|
||||
"status": 400,
|
||||
"nativeCalls": 0
|
||||
},
|
||||
{
|
||||
"field": "keepaliveRetries",
|
||||
"value": -1,
|
||||
"status": 400,
|
||||
"nativeCalls": 0
|
||||
},
|
||||
{
|
||||
"field": "keepaliveRetries",
|
||||
"value": 1.5,
|
||||
"status": 400,
|
||||
"nativeCalls": 0
|
||||
},
|
||||
{
|
||||
"field": "keepaliveRetries",
|
||||
"value": 4294967296,
|
||||
"status": 400,
|
||||
"nativeCalls": 0
|
||||
},
|
||||
{
|
||||
"operation": "create",
|
||||
"params": {},
|
||||
"keepalive": "10s,10",
|
||||
"status": 201
|
||||
},
|
||||
{
|
||||
"operation": "create",
|
||||
"params": {
|
||||
"keepaliveInterval": 60
|
||||
},
|
||||
"keepalive": "60s,10",
|
||||
"status": 201
|
||||
},
|
||||
{
|
||||
"operation": "create",
|
||||
"params": {
|
||||
"keepaliveRetries": 3
|
||||
},
|
||||
"keepalive": "10s,3",
|
||||
"status": 201
|
||||
},
|
||||
{
|
||||
"operation": "create",
|
||||
"params": {
|
||||
"keepaliveInterval": 90,
|
||||
"keepaliveRetries": 7
|
||||
},
|
||||
"keepalive": "90s,7",
|
||||
"status": 201
|
||||
},
|
||||
{
|
||||
"operation": "patch-other-field",
|
||||
"nativeKeepalivePreserved": "1m,3",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"operation": "read-minutes",
|
||||
"seconds": 60,
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"scope": "Production compiled GRE create/patch using isolated PostgreSQL and native HTTP fixture",
|
||||
"passed": true,
|
||||
"nativeWrites": 5,
|
||||
"databaseAndRoleRemoved": true
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.210",
|
||||
"runtime": "ux-gre210",
|
||||
"cases": [
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"defaultCreateStatus": 201,
|
||||
"defaultNative": "10s,10",
|
||||
"minutesReadAsSeconds": 60,
|
||||
"patchOtherFieldPreservesNative": "1m,3",
|
||||
"invalidIntervalStatus": 400,
|
||||
"invalidPatchLeavesNativeUnchanged": true,
|
||||
"catalogSeconds": 60
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"defaultCreateStatus": 201,
|
||||
"defaultNative": "10s,10",
|
||||
"minutesReadAsSeconds": 60,
|
||||
"patchOtherFieldPreservesNative": "1m,3",
|
||||
"invalidIntervalStatus": 400,
|
||||
"invalidPatchLeavesNativeUnchanged": true,
|
||||
"catalogSeconds": 60
|
||||
}
|
||||
],
|
||||
"uiCases": [
|
||||
{
|
||||
"section": "gre",
|
||||
"width": 1440,
|
||||
"actualNativeRead": true,
|
||||
"minutesFormSeconds": 60,
|
||||
"invalidInputPreservesForm": true,
|
||||
"mutationRequests": 0,
|
||||
"pageErrors": 0
|
||||
},
|
||||
{
|
||||
"section": "filters",
|
||||
"width": 1440,
|
||||
"actualNativeRead": true,
|
||||
"configuredGreCatalogAccepted": true,
|
||||
"mutationRequests": 0,
|
||||
"pageErrors": 0
|
||||
},
|
||||
{
|
||||
"section": "gre",
|
||||
"width": 320,
|
||||
"actualNativeRead": true,
|
||||
"minutesFormSeconds": 60,
|
||||
"invalidInputPreservesForm": true,
|
||||
"mutationRequests": 0,
|
||||
"pageErrors": 0
|
||||
},
|
||||
{
|
||||
"section": "filters",
|
||||
"width": 320,
|
||||
"actualNativeRead": true,
|
||||
"configuredGreCatalogAccepted": true,
|
||||
"mutationRequests": 0,
|
||||
"pageErrors": 0
|
||||
}
|
||||
],
|
||||
"scope": "Disabled own native GRE; defaults, unit parsing, patch preservation and UI validation; not data-plane",
|
||||
"cleanupCompleted": true
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.210",
|
||||
"cases": [
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "0",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "value of keepalive-interval is out of range (00:00:01 .. 7101w3d06:28:15)"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "none",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "invalid time value for argument keepalive-interval"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "invalid time value for argument keepalive-interval"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "0s,0",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "value of keepalive-interval is out of range (00:00:01 .. 7101w3d06:28:15)"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "1s,0",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "value of keepalive-retries out of range (1..4294967295)"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "1m,3",
|
||||
"status": 200,
|
||||
"observed": "1m,3"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "500ms,3",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "value of keepalive-interval is out of range (00:00:01 .. 7101w3d06:28:15)"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "00:01:00,3",
|
||||
"status": 200,
|
||||
"observed": "1m,3"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "0",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "value of keepalive-interval is out of range (00:00:01 .. 7101w3d06:28:15)"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "none",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "invalid time value for argument keepalive-interval"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "invalid time value for argument keepalive-interval"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "0s,0",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "value of keepalive-interval is out of range (00:00:01 .. 7101w3d06:28:15)"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "1s,0",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "value of keepalive-retries out of range (1..4294967295)"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "1m,3",
|
||||
"status": 200,
|
||||
"observed": "1m,3"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "500ms,3",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "value of keepalive-interval is out of range (00:00:01 .. 7101w3d06:28:15)"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "00:01:00,3",
|
||||
"status": 200,
|
||||
"observed": "1m,3"
|
||||
}
|
||||
],
|
||||
"scope": "Direct native syntax exploration on disabled owned interfaces; application fixes not deployed",
|
||||
"cleanupCompleted": true
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.210",
|
||||
"cases": [
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "disabled",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "invalid time value for argument keepalive-interval"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "no",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "invalid time value for argument keepalive-interval"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "1.5s,3",
|
||||
"status": 200,
|
||||
"observed": "1s,3"
|
||||
},
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"value": "1s500ms,3",
|
||||
"status": 200,
|
||||
"observed": "1s,3"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "disabled",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "invalid time value for argument keepalive-interval"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "no",
|
||||
"status": 400,
|
||||
"observed": "10s,3",
|
||||
"error": "invalid time value for argument keepalive-interval"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "1.5s,3",
|
||||
"status": 200,
|
||||
"observed": "1s,3"
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"value": "1s500ms,3",
|
||||
"status": 200,
|
||||
"observed": "1s,3"
|
||||
}
|
||||
],
|
||||
"scope": "Direct native syntax exploration on disabled owned interfaces; application fixes not deployed",
|
||||
"cleanupCompleted": false
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.210",
|
||||
"commands": [
|
||||
"npm --prefix backend run test:live-maps",
|
||||
"npx tsx lib/filters-response.test.ts",
|
||||
"npx tsx lib/network-map-response.test.ts"
|
||||
],
|
||||
"passed": true,
|
||||
"networkMapUnitCases": 5,
|
||||
"frontendTypecheckPassed": true,
|
||||
"backendBuildPassed": true,
|
||||
"eslintErrors": 0
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.210",
|
||||
"kinds": [
|
||||
{
|
||||
"kind": "backend",
|
||||
"matched": 475
|
||||
},
|
||||
{
|
||||
"kind": "frontend",
|
||||
"matched": 527
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import {filtersGreResponseSchema} from './filters-response'
|
||||
const t={id:'*1',serverId:'1',name:'own210',localAddress:'192.0.2.1',remoteAddress:'192.0.2.2',localInnerIp:'10.0.0.1/30',remoteInnerIp:'10.0.0.2/30',poolId:'',ipsec:{secret:''},mtu:1476,keepaliveInterval:60,keepaliveRetries:3,dscp:'inherit',clampTcpMss:true,allowFastPath:false,comment:'',enabled:true,status:'up'}
|
||||
assert.equal(filtersGreResponseSchema.safeParse({tunnels:[t]}).success,true)
|
||||
assert.equal(filtersGreResponseSchema.safeParse({tunnels:[{...t,ipsec:null}]}).success,true)
|
||||
assert.equal(filtersGreResponseSchema.safeParse({tunnels:[{...t,ipsec:{secret:'must-not-be-in-catalog'}}]}).success,false)
|
||||
console.log('Filters GRE catalog: configured IPsec accepted without key material')
|
||||
@@ -15,7 +15,8 @@ export const filtersMutationSchema = z.object({ ok: z.literal(true), rules: filt
|
||||
export const filtersGreResponseSchema = z.object({
|
||||
tunnels: z.array(z.object({
|
||||
id: z.string().min(1), serverId: z.string().min(1), name: z.string(), localAddress: z.string(), remoteAddress: z.string(),
|
||||
localInnerIp: z.string(), remoteInnerIp: z.string(), poolId: z.string(), ipsec: z.null(), mtu: z.number().int().nonnegative(),
|
||||
localInnerIp: z.string(), remoteInnerIp: z.string(), poolId: z.string(),
|
||||
ipsec: z.object({ secret: z.literal("") }).nullable(), mtu: z.number().int().nonnegative(),
|
||||
keepaliveInterval: z.number().int().nonnegative(), keepaliveRetries: z.number().int().nonnegative(),
|
||||
dscp: z.union([z.literal("inherit"), z.number().int().min(0).max(63)]), clampTcpMss: z.boolean(), allowFastPath: z.boolean(),
|
||||
comment: z.string(), enabled: z.boolean(), status: z.enum(["up", "down", "degraded"]),
|
||||
|
||||
@@ -2,6 +2,11 @@ import {test} from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import {validateNetworkMapSnapshot} from "./network-map-response"
|
||||
const server={id:1,name:"Router",host:"192.0.2.1",type:"home-router",country:"RU",enabled:true,status:null,latency:0}
|
||||
test("network map retains configured GRE IPsec without key material",()=>{
|
||||
const tunnel={id:"*1",serverId:"1",name:"own210",localAddress:"192.0.2.1",remoteAddress:"192.0.2.2",localInnerIp:"10.0.0.1/30",remoteInnerIp:"10.0.0.2/30",poolId:"",ipsec:{secret:""},mtu:1476,keepaliveInterval:60,keepaliveRetries:3,dscp:"inherit",clampTcpMss:true,allowFastPath:false,comment:"",enabled:true,status:"up"}
|
||||
const result=validateNetworkMapSnapshot([server],{tunnels:[tunnel],failures:[]},{probes:[]})
|
||||
assert.deepEqual(result.tunnels[0].ipsec,{secret:""})
|
||||
})
|
||||
test("network map requires complete envelopes and preserves zero/null",()=>{
|
||||
const result=validateNetworkMapSnapshot([server],{tunnels:[],failures:[]},{probes:[]})
|
||||
assert.equal(result.servers[0].latency,0);assert.equal(result.servers[0].status,null)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
for (const kind of ["gre", "recursive"] as const) {
|
||||
test(`Filters: каталог ${kind} с masked IPsec сохраняется при сбое и блокирует маршрут/копирование`, async ({ page }) => {
|
||||
let phase = 0, writes = 0, catalogReads = 0
|
||||
const tunnel = (id: string) => ({ id: "t" + id, serverId: id, name: "GRE-" + id, localAddress: "192.0.2." + id, remoteAddress: "198.51.100.1", localInnerIp: "10.0.0.1/30", remoteInnerIp: "10.0.0.2/30", poolId: "", ipsec: { secret: "" }, mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 3, dscp: "inherit", clampTcpMss: true, allowFastPath: true, comment: "", enabled: true, status: "up" })
|
||||
const rec = { id: "rec1", dstAddress: "10.0.0.2/32", gateway: "198.51.100.1", distance: 1, routingTable: "main", comment: "Рекурсивный шлюз", disabled: false }
|
||||
const rule = { id: "rule-1", community: "65001:101", gateway: "10.0.0.2", gatewayTunnelId: kind === "gre" ? "t1" : "rec:rec1", action: "route", description: "Маршрут" }
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", route => {
|
||||
const url = new URL(route.request().url()), path = url.pathname, id = url.searchParams.get("serverId") ?? "1"
|
||||
if (path === "/api/filters/rules" && route.request().method() === "PUT") { writes++; return route.fulfill({ json: { ok: true, rules: route.request().postDataJSON().rules } }) }
|
||||
const catalog = kind === "gre" ? "/api/filters/gre-tunnels" : "/api/recursive-routes"
|
||||
if (path === catalog) catalogReads++
|
||||
if (path === catalog && phase === 1) return route.fulfill({ status: 503, json: { error: "Каталог недоступен тест" } })
|
||||
if (path === catalog && phase === 2) return route.fulfill({ json: kind === "gre" ? { tunnels: [{ ...tunnel(id), name: null }] } : { routes: [rec], stale: true } })
|
||||
return route.fulfill({ json: path === "/api/filters/rules" ? { rulesets: [{ serverId: "1", rules: [rule] }, { serverId: "2", rules: [] }], live: true, greTunnels: [] }
|
||||
: path === "/api/servers" ? [1, 2].map(i => ({ id: i, name: "Роутер " + i, host: "192.0.2." + i, type: "exit-node", enabled: true, country: "RU", site: "TEST", asn: "", status: "online", latency: 1 }))
|
||||
: path === "/api/filters/gre-tunnels" ? { tunnels: kind === "gre" && phase !== 3 ? [tunnel(id)] : [] }
|
||||
: path === "/api/recursive-routes" ? { routes: kind === "recursive" && phase !== 3 ? [rec] : [], stale: false }
|
||||
: path === "/api/auth/config" ? { required: false } : {} })
|
||||
})
|
||||
await page.goto("/filters")
|
||||
const row = page.locator("tr[data-row-id]").first()
|
||||
await expect(row).toContainText(kind === "gre" ? "GRE-1" : rec.dstAddress)
|
||||
phase = 1
|
||||
await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||
await expect(page.getByRole("alert").filter({ hasText: "Каталог недоступен тест" })).toBeVisible()
|
||||
await expect(row).toContainText(kind === "gre" ? "GRE-1" : rec.dstAddress)
|
||||
await row.hover()
|
||||
await row.getByRole("button", { name: "Редактировать правило", exact: true }).click()
|
||||
const dialog = page.getByRole("dialog")
|
||||
await dialog.getByRole("textbox", { name: "Описание правила" }).fill("Сохранить маршрут")
|
||||
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
|
||||
await expect(dialog.getByRole("alert")).toContainText("Шлюзы устройства не подтверждены")
|
||||
expect(writes).toBe(0)
|
||||
await dialog.getByRole("button", { name: "Отмена", exact: true }).click()
|
||||
await page.getByRole("button", { name: "Копировать", exact: true }).click()
|
||||
await dialog.getByRole("button", { name: /Роутер 2/ }).click()
|
||||
const copy = dialog.getByRole("button", { name: "Копировать 1 правил", exact: true })
|
||||
await expect(copy).toBeDisabled()
|
||||
phase = 2
|
||||
let priorReads = catalogReads
|
||||
await dialog.getByRole("button", { name: "Обновить шлюзы", exact: true }).click()
|
||||
await expect.poll(() => catalogReads).toBeGreaterThanOrEqual(priorReads + 2)
|
||||
await expect(copy).toBeDisabled()
|
||||
await expect(dialog.getByRole("alert")).toContainText("сопоставление шлюзов не подтверждены")
|
||||
phase = 3
|
||||
priorReads = catalogReads
|
||||
await dialog.getByRole("button", { name: "Обновить шлюзы", exact: true }).click()
|
||||
await expect.poll(() => catalogReads).toBeGreaterThanOrEqual(priorReads + 2)
|
||||
await page.evaluate(() => new Promise<void>(resolve => requestAnimationFrame(() => resolve())))
|
||||
await expect(copy).toBeDisabled()
|
||||
phase = 4
|
||||
await dialog.getByRole("button", { name: "Обновить шлюзы", exact: true }).click()
|
||||
await expect(copy).toBeEnabled()
|
||||
await expect(dialog.getByRole("alert")).toHaveCount(0)
|
||||
await copy.click()
|
||||
await expect(dialog).toHaveCount(0)
|
||||
expect(writes).toBe(1)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import {test,expect} from '@playwright/test'
|
||||
for(const width of [1440,320])test(`GRE: неверный keepalive сохраняет форму без запроса ${width}`,async({page})=>{
|
||||
let writes=0;const tunnel={id:'*1',serverId:'1',name:'own210-gre',localAddress:'192.0.2.1',remoteAddress:'192.0.2.2',localInnerIp:'',remoteInnerIp:'',poolId:'live',ipsec:null,mtu:1476,keepaliveInterval:60,keepaliveRetries:3,dscp:'inherit',clampTcpMss:true,allowFastPath:false,comment:'own210',enabled:false,status:'down'}
|
||||
await page.route('**/health',r=>r.fulfill({json:{ok:true}}));await page.route('**/api/**',r=>{if(r.request().method()!=='GET')writes++;const p=new URL(r.request().url()).pathname;return r.fulfill({json:p==='/api/auth/config'?{required:false}:p==='/api/servers'?[{id:1,name:'Own210 router',host:'192.0.2.1',enabled:true,country:'RU'}]:p==='/api/gre/tunnels'?{tunnels:[tunnel]}:{}})})
|
||||
await page.setViewportSize({width,height:1000});await page.goto('/gre?server=1');await page.getByRole('button',{name:'Действия: own210-gre',exact:true}).click();await page.getByRole('menuitem',{name:'Редактировать',exact:true}).click();const form=page.getByRole('dialog');await form.getByRole('button',{name:'Дополнительно',exact:true}).click();await expect(form.getByLabel('Keepalive, с')).toHaveValue('60');await expect(form).not.toContainText('0 = откл.');await form.getByLabel('Комментарий').fill('сохранённый ввод');
|
||||
for(const field of ['Keepalive, с','Попытки']){await form.getByLabel(field).fill('0');await form.getByRole('button',{name:'Сохранить',exact:true}).click();await expect(form.getByRole('alert')).toContainText('целые значения от 1');await expect(form.getByLabel('Комментарий')).toHaveValue('сохранённый ввод');await form.getByLabel(field).fill(field==='Попытки'?'3':'60')}
|
||||
expect(writes).toBe(0);expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true)
|
||||
})
|
||||
Reference in new issue
Block a user